Skip to content

feat(remote): phone-first mobile console + push notifications#614

Open
bborn wants to merge 4 commits into
mainfrom
task/4435-mobileremote
Open

feat(remote): phone-first mobile console + push notifications#614
bborn wants to merge 4 commits into
mainfrom
task/4435-mobileremote

Conversation

@bborn

@bborn bborn commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Why

Today, keeping TaskYou moving from a phone means SSH-ing into the box over Tailscale and fighting the TUI in Termius — almost unusable. The goal: leave the keyboard, go for a walk, and keep work moving — get pinged when an agent needs you, then reply/approve/retry from your phone.

What

Two pieces, both stdlib-only and always shipped from a plain go build (no Node build, no React, no extra services):

📱 Mobile console — GET /m

A self-contained, phone-first HTML/CSS/vanilla-JS page embedded in the binary that drives the existing JSON API. ty serve over Tailscale now gives a real phone UI:

  • Board ordered for the on-the-go case: Needs you (blocked) first, then Running, Queue, Recent.
  • Tap a task to reply to a blocked agent, Approve/Continue, Retry a finished task, view the PR link, summary, and latest output.
  • Auto-refreshes every 5s; supports deep links (/m?task=N).

mobile board

🔔 Push notifications — internal/notify

Opt-in pushes at the moments a walking user must act — blocked, auth_required, completed, failed (noisy created/started/updated excluded by default):

  • ntfy (free iOS/Android app, no account) or arbitrary webhook (Slack/Discord/Telegram/Zapier).
  • Notifications deep-link into the mobile console (/m?task=N) so a tap lands you on the task ready to reply.
  • Settings read fresh on every send (toggle takes effect with no restart).

Wired into the events emitter (daemon executor and web routine runs), plus settings and a new command:

ty settings set notify_target https://ntfy.sh/ty-7f3k9
ty settings set notify_enabled true
ty settings set notify_url http://<tailscale-host>:8080
ty notify test

Files

  • internal/web/mobile.go, internal/web/mobile.html — embedded console + /m handler
  • internal/web/server.go — register /m route (before the catch-all React handler)
  • internal/notify/ — provider-agnostic notifier (ntfy/webhook), event filtering, deep links
  • internal/events/events.goSetNotifier + fan-out from Emit/Wait
  • internal/executor/executor.go, internal/web/routines.go — attach notifier to emitters
  • cmd/task/main.goty notify command + notify_* settings; completion.go — shell completion
  • Tests: mobile_test.go, internal/notify/notify_test.go, internal/events/notify_test.go

Testing

  • go build ./...
  • go test ./internal/notify/ ./internal/events/ ./internal/web/
  • golangci-lint run (v2.8.0, matches CI) — 0 issues ✅
  • Live smoke test: ran ty serve, confirmed /m renders the real board and /api/board returns data; verified the rendered mobile UI in a 390×844 viewport.

Notes / follow-ups

  • The console reuses the existing API surface, so it stays correct as those handlers evolve.
  • Reaching /m still requires network access to the host (Tailscale/SSH tunnel) — auth/exposure is unchanged from ty serve today.
  • Possible follow-ups: add /m to the desktop shell nav, badge counts in notifications, and an "open console" hint in ty serve startup output.

🤖 Generated with Claude Code

bborn and others added 2 commits June 23, 2026 08:50
Make TaskYou usable on a walk. Two pieces, both zero-dependency and always
shipped from a plain `go build`:

Mobile console (GET /m)
- Self-contained HTML/CSS/vanilla-JS page embedded in the binary, driving the
  existing JSON API. No Node build, no React — `ty serve` over Tailscale now
  gives a usable phone UI instead of a cramped SSH/TUI session.
- Phone-first board: "Needs you" (blocked) surfaced first, then Running, Queue,
  Recent. Tap a task to reply to a blocked agent, approve/continue, retry a
  finished task, or read the latest output. Polls every 5s; deep-links to a task.

Push notifications (internal/notify)
- Opt-in pushes when a task blocks, needs sign-in, completes, or fails — the
  moments where a walking user must act. ntfy (free iOS/Android app, no account)
  or arbitrary webhook (Slack/Discord/Telegram/Zapier).
- Notifications deep-link into the mobile console (/m?task=N) so a tap opens the
  task ready to reply.
- Wired into the events emitter (daemon executor + web routine runs), settings,
  and a new `ty notify` command (`ty notify test`). Config via `ty settings`:
  notify_enabled / notify_provider / notify_target / notify_events / notify_url.

Tests: mobile route + console contents, notifier providers/filtering/deep-links,
emitter fan-out to push (and no-notifier safety). Lint clean (golangci-lint v2.8.0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	cmd/task/completion.go
#	cmd/task/completion_test.go
#	cmd/task/main.go
@bborn

bborn commented Jun 23, 2026

Copy link
Copy Markdown
Owner Author

✅ Merge conflicts resolved + full QA

Merged origin/main into the branch (commit 179b1236). Conflicts were all in cmd/task where main added the new http_api_port / http_api_disabled settings alongside this PR's notify_* settings — resolved by keeping both sets:

  • completion.go — both groups of completion keys
  • completion_test.go — expected key count now 10 (3 base + 5 notify + 2 http)
  • main.go — both groups in the settings set help text, validation switch, and the "Available:" hint

QA results (against the merged tree)

Check Result
go build ./...
go test (notify, events, web, cmd/task, config, executor) ✅ all pass
golangci-lint run (v2.8.0 — matches CI) ✅ 0 issues
Live ty serveGET /m ✅ 200, renders real board (backlog 7 / processing 5 / blocked 47 / done 406)
Deep link /m?task=N → detail view ✅ summary, PR link, reply box + Approve/Continue, latest output
End-to-end push (ty notify test → local ntfy catcher) ✅ received with Title, Priority, and deep-link Click: …/m?task=0

Updated screenshots (390px-wide phone viewport, live data)

Board — "Needs you" first
board

Blocked-task detail — reply UI
detail

Detail (taller viewport) — Send reply / Approve / Continue + latest output
detail-tall

🤖 Generated with Claude Code

Agent summaries are written in Markdown, but the mobile console showed them
raw (literal **bold** and - bullets). Add a tiny, dependency-free, XSS-safe
Markdown renderer (escape-first; bold/italic/inline-code/links + heading,
bullet/ordered list, fenced-code, and paragraph blocks) and use it for the
summary. Logs stay verbatim monospace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bborn

bborn commented Jun 23, 2026

Copy link
Copy Markdown
Owner Author

📝 Follow-up: render Markdown in the task summary

Caught during QA — agent summaries are written in Markdown, but the console was showing them raw (literal **bold** and - bullets). Added a tiny, dependency-free, XSS-safe Markdown renderer (escape-first; bold / italic / inline-code / links + heading, bullet & ordered lists, fenced code, and paragraph blocks) and applied it to the summary. Terminal logs stay verbatim monospace. Commit a97f3971.

Before After
before after

Re-QA: go build ✅ · go test ./internal/web/ ✅ (test now asserts the renderer ships) · golangci-lint v2.8.0 ✅ 0 issues · live render shows bold labels + real bullet list, 0 browser console errors.

🤖 Generated with Claude Code

…very race

Ports the two wins from PR #621 onto the mobile-console notifier:

1. One-tap unblock. ntfy pushes for actionable events (blocked/auth_required)
   now carry an Actions header: an http button that POSTs the canned reply to
   the existing POST /api/tasks/{id}/input (resuming the agent without opening
   anything), plus a view button deep-linking into the mobile console (/m) for
   a custom reply — console and one-tap, together. Reply text is configurable
   via notify_reply (default "continue"); validated live against ntfy.sh.

2. CLI-exit delivery race fix. The notifier read its settings inside the async
   delivery goroutine, but short-lived CLI/MCP commands defer db.Close() the
   instant Run returns — before PersistentPostRun flushes the emitter wait
   group — so the goroutine hit a closed DB, saw notifications as disabled, and
   silently dropped the push (daemon/web runs were unaffected; their DB stays
   open). Notifier.Prepare now reads settings + builds the request synchronously
   and returns a network-only delivery closure; events.Emit calls it before
   spawning the goroutine.

Existing header/webhook behavior and tests are unchanged; adds tests for the
action button (default + custom reply, and no action when no base URL is set).
@bborn

bborn commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

Pushed a commit onto this branch (e0a1894) that folds in the two unique wins from #621, so #621 can be closed as a duplicate:

1. One-tap unblock action button. ntfy pushes for actionable events (blocked/auth_required) now carry an Actions header with:

  • an http button that POSTs the canned reply to the existing POST /api/tasks/{id}/input — resumes the agent without opening anything, and
  • a view button deep-linking into the mobile console (/m?task=N) for a custom reply.

So you get the console and true one-tap, together. Reply text is configurable via notify_reply (default continue). I validated the exact Actions header format live against ntfy.sh (http POST + JSON body + view action all parse correctly).

2. CLI-exit delivery race fix. The notifier read its settings inside the async delivery goroutine, but short-lived CLI/MCP commands defer db.Close() the instant Run returns — before PersistentPostRun flushes the emitter wait group — so the goroutine hit a closed DB, saw notifications as "disabled", and silently dropped the push. (Daemon/web routine runs were unaffected — their DB stays open.) Notifier.Prepare now reads settings + builds the request synchronously and returns a network-only delivery closure that events.Emit runs on the wait group.

Existing header/webhook behavior and tests are unchanged; added tests for the action button. go build, go test ./internal/notify/ ./internal/events/, gofmt, and golangci-lint run (v2.8.0) all pass.

Skipped from #621 on purpose: a Telegram-native provider (your webhook provider already covers a Telegram bridge) and reason-enrichment from task logs (kept the change surface small).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant