Team Tracker
Team Tracker records active minutes (1-minute ticks) while you work in VS Code or Cursor, classifies them per team workspace as internal or external based on the Git remote of the active editor, and sends aggregates to the SnakeFlow team API. It is optional and off until you sign in and enable it.
Minute counting is independent of Wake Lock → (OS sleep prevention): the tracker does not infer “human active” from whether the wake lock is held.
Requirements
- SnakeFlow: Team Tracker — Login (GitHub OAuth via the editor).
devManager.team.enabledset to true (the login command can turn this on for you).- For OS-wide mouse and keyboard coverage outside the editor: ActivityWatch (
aw-qt) on your machine (recommended; see below).
ActivityWatch and the status bar
When ActivityWatch is running, the extension can see input activity across apps (browser, terminal outside VS Code, etc.) so you do not drop to idle only because the editor has no events. If the window watcher (aw-watcher-window_* bucket) is present, the extension also reads the currently focused application name and window title once per minute (cached briefly) for app allowlist classification and for the status-bar tooltip.
- Click the Team Tracker status-bar item to start
aw-qt(if it is installed) or stop ActivityWatch (terminates the local ActivityWatch processes on your OS). - If
aw-qtis not found, you get a prompt to open the download page. - SnakeFlow: Team Tracker — Show Activity Log (Command Palette) opens the detailed trace (
ACTIVE/IDLE/GATEDlines).
Hard gate (requireActivityWatch)
When devManager.team.requireActivityWatch is true (default), no minutes are counted unless the ActivityWatch REST API is reachable at activityWatchUrl. The status bar shows a red blocked icon and the tooltip explains that time is not being recorded until you start ActivityWatch.
Turn requireActivityWatch off only for roles that should not be forced (for example admins on their own machines).
What counts as “active”
Each 60-second tick first checks optional Sleep window (forces IDLE if local time falls inside idleSleepWindow).
Otherwise the collector needs ActivityWatch reachable (see Hard gate). It then evaluates three signals:
| Signal | Source | Role |
|---|---|---|
| AW input | aw-watcher-afk / input buckets via REST | “ActivityWatch saw keyboard/mouse within the idle window.” |
| OS idle | Windows GetLastInputInfo, macOS HIDIdleTime, Linux xprintidle when installed | “True last user input age” — robust against synthetic hooks that fool AW alone. |
| IDE focus | VS Code window.state.focused + last focus timestamp | Corroborates AW when the editor had OS focus recently. |
A tick refreshes the activity clock (lastAwAt) only when trustworthy evaluates true — i.e. one of:
- AW and OS agree — both report recent input within the idle window (strongest).
- OS alone (Windows / macOS) — OS idle API says recent input (AW can disagree; we still count activity on these platforms).
- AW + recent IDE focus — AW says active and this VS Code window had OS focus within the idle window (blocks “synthetic input while you are away from the editor”).
- Linux fallback — when the OS idle helper is unavailable (no
xprintidle), AW-only behaviour is preserved so Linux users are not regressed.
If none of the above apply for longer than your effective idle window, the minute is IDLE (no increment). The effective window is the minimum idle_window_minutes from all team workspaces you belong to (server-side admin setting); with no workspace memberships yet, a 5-minute built-in default applies until you join.
Each UTC day is capped by the backend — exact limits depend on your plan; workspace admins can confirm in the Cloud dashboard or the API reference.
Internal vs external
For each workspace you belong to, a tick is internal only when both are true:
- Repo match — the current GitHub-style remote of the active text editor (
owner/repo) matches that workspace’s repo patterns (glob-style lists configured per workspace; see Cloud sync → Repo patterns). - App match (if configured) — that workspace’s app allowlist is empty (no filter), or at least one regex pattern matches the ActivityWatch window
appname or window title (case-insensitive). Patterns are stored on the server per workspace (admins edit them; every member receives them via/api/team/me).
Otherwise the tick is +1 external for that workspace:
- Wrong repo → external with the real
owner/repolabel when known. - Wrong app (non-empty allowlist and no pattern matches the focused window) → external with a synthetic label
_app:ApplicationNameso the backend can store per-source external minutes alongside real repos.
Empty app allowlist means “any application counts” for the app filter; only repo patterns decide internal vs external.
Note: When ActivityWatch marks you active while you are focused in another app, repo classification still uses the last active editor in Cursor/VS Code. Keep a relevant file focused when you care about correct internal routing. If a workspace has a non-empty app allowlist but the window watcher is not installed or has no bucket, ActivityWatch may not report app/title → the tick is treated as not matching the allowlist and counts as external for that workspace.
App allowlist (admin)
Workspace admins maintain the list of regex strings on the server:
- SnakeFlow Cloud → Team tab → workspace toolbar → Edit app allowlist (camera icon), which opens the same flow as the command below.
- SnakeFlow: Team Tracker — Edit App Allowlist (Command Palette) — comma-separated regexes; leave empty to clear the list (allow all apps again).
The server validates each pattern as a JavaScript RegExp (case-insensitive flag) and rejects invalid syntax. There is a hard limit on how many patterns and how long each string may be — check the Team / workspace endpoints in the API reference for current caps.
Tip: Patterns are matched against both the executable / app name and the window title (for example Cursor or \.tsx in the title), similar in spirit to ActivityWatch categorization but enforced by your team’s allowlist for SnakeFlow reporting.
Settings
| Setting | Type | Default | Description |
|---|---|---|---|
devManager.team.enabled | boolean | false | Master switch for Team Tracker. |
devManager.team.idleWindowMinutes | number | 5 | Declared idle window (1–15); changing it restarts the collector. Effective window with memberships = min server idle_window_minutes — see What counts as active. |
devManager.team.useActivityWatch | boolean | true | Query the local ActivityWatch server for OS-wide input. |
devManager.team.requireActivityWatch | boolean | true | If true, no time is recorded when ActivityWatch is not reachable. |
devManager.team.activityWatchUrl | string | http://localhost:5600 | Base URL of aw-server. |
devManager.team.requireTracking | boolean | false | When true, warn if tracking is off and no session started. |
devManager.team.idleSleepWindow | string | "" | Local-time HH:MM-HH:MM window during which tracking is force-IDLE — see Sleep window. |
Changing these keys triggers a collector restart so new values apply without reloading the window.
Sleep window
devManager.team.idleSleepWindow (format HH:MM-HH:MM in local time, default empty) is a belt-and-braces guard against false-positive activity at night. While the current local time falls inside this interval, the collector forces IDLE regardless of ActivityWatch, OS idle time, or VS Code focus.
Use it when:
- Synthetic input from remote-control or “anti-idle” tools (AnyDesk, mouse jigglers, certain drivers) misleads ActivityWatch into reporting
not-afkwhile you sleep. - A long-running task or never-released wake lock keeps the OS awake overnight.
The window wraps over midnight when end < start — 23:00-07:00 covers a typical sleep schedule. Empty string disables the override.
This works alongside the dual-factor activity rules in What counts as active: even with the sleep window disabled, the collector prefers corroborated signals (AW + OS, OS alone on Windows/macOS, or AW + recent VS Code focus) so lone synthetic input from remote tools is unlikely to count a minute.
Commands
| Command | Description |
|---|---|
| SnakeFlow: Team Tracker — Login | Sign in; can enable team tracking. |
| SnakeFlow: Team Tracker — Logout | Stop tracking and sign out. |
| SnakeFlow: Team Tracker — Status | Quick summary in a notification. |
| SnakeFlow: Team Tracker — Show Activity Log | Output channel with per-minute reasons. |
| SnakeFlow: Team Tracker — Start/Stop ActivityWatch | Same as status-bar toggle (palette access). |
| SnakeFlow: Team Tracker — Edit App Allowlist | Workspace admin: edit regex allowlist for ActivityWatch window app/title (server-side). |
| SnakeFlow: Team Tracker — Create Workspace | Admin: new workspace + invite. |
| SnakeFlow: Team Tracker — Join Workspace | Join with invite code. |
| SnakeFlow: Team Tracker — Manage Workspaces | List / switch context. |
| SnakeFlow: Team Tracker — Sync Commits (admin) | Push aggregated commit counts for dashboards. |
| SnakeFlow: Team Tracker — Sync Closed Issues (admin) | Push closed GitHub issue counts (assignee + UTC closed day) for dashboards. |
Commit and closed-issue sync (admin)
Workspace admins with Team Tracker enabled and a GitHub session can sync aggregated GitHub data into SnakeFlow Cloud (D1). The backend never receives your GitHub token; the extension queries GitHub Search and POSTs counts.
| Metric | GitHub query (per member, per UTC day) | Stored as | Internal vs external |
|---|---|---|---|
| Commits | author:{login} author-date:{YYYY-MM-DD} | daily_commits | Repo matches workspace repo patterns |
| Closed issues | assignee:{login} is:issue is:closed closed:{YYYY-MM-DD} | daily_closed_issues | Same repo patterns |
- Assignee required for closed issues: unassigned issues are not counted.
- Who closed does not matter — only assignee and closed date.
- Sync runs hourly while tracking is on (admin workspaces) and can be triggered manually via the commands above (last 7 UTC days by default).
- SnakeFlow Cloud → Activity shows internal/external columns for both metrics.
Hourly heatmap and member time zones
The SnakeFlow Cloud → Activity view shows a per-day 24-hour strip (0h–23h) for each member. The backend stores hourly buckets in UTC (the hour at flush time on the server). That alone would misalign “9 AM local work” for teammates in different regions.
On every successful POST /api/team/active-minutes flush, the extension sends the member’s IANA time zone (from Intl.DateTimeFormat().resolvedOptions().timeZone, for example Europe/Kyiv). The API stores it on the workspace_members row for that member (last value wins).
The dashboard then converts each stored (UTC day, UTC hour) bucket into that member’s local calendar hour before drawing the strip. The result is a shared 0–23 “local working hours” axis: if one person starts at 09:00 in Kyiv and another at 09:00 in London, both appear in the same column (hour 9), which matches how teams usually read “who was active when” in their own heads.
- Legacy members who have never pushed with a client that sends
timezoneshownullin the API; the UI treats that as UTC for the conversion until their next flush updates the field. - Changing the OS time zone on a machine updates the value on the next push; historical buckets are reinterpreted with the latest stored zone (acceptable for a team-activity overview).
- Per-day totals (
daily_minutes) stay on UTC calendar days from the server; only the hourly visualization uses the member zone shift.
There is no separate “workspace time zone” setting for this feature—the alignment comes from each member’s reported zone.
See also
- Wake Lock → — OS sleep prevention (separate subsystem)
- All settings → — full
devManager.*tables - Command palette reference →