Skip to content

Commit dc0e342

Browse files
feat(viewer,notifications): git changes view and push notifications (#156)
Implements both #151 and #153. Git changes view in the code viewer — browse staged, modified, and untracked files with inline diffs (unified view with full-file toggle), per-line blame annotations, and commit log. Accessible via the Files/Changes tabs in the sidebar or the command palette. Push notifications for command completion — uses Web Push (VAPID) to deliver native notifications when commands finish, even when the app is backgrounded on mobile. Server monitors the process tree (descendant count via ps+awk) every 2 seconds to detect when child processes exit. Service worker handles push display, notification merging, and Badge API. Also includes 38 new tests, updated API/architecture/README docs, and a web-push dependency. Closes #151 Closes #153 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent aab46a4 commit dc0e342

35 files changed

Lines changed: 3810 additions & 182 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ termbeam -i # interactive setup wizard
7575
- **File upload** — send files from your phone to the session's working directory
7676
- **File browser & download** — browse files in a session's working directory from the side panel and download them to your device
7777
- **Markdown viewer** — preview `.md` files rendered with GitHub Flavored Markdown directly in the browser
78+
- **Git changes view** — view git status, diffs, blame, and commit history in the code viewer. Toggle between Files and Changes tabs, view staged/unstaged diffs with syntax highlighting, and see per-line blame annotations
79+
- **Push notifications** — get native push notifications on your phone when commands complete, even when the app is in the background. Uses Web Push API with VAPID authentication
7880
- **Completion notifications** — browser alerts when background commands finish
7981
- **30 color themes** with adjustable font size
8082
- **Port preview** — reverse-proxy a local web server through TermBeam

docs/api.md

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,233 @@ Serve a previously uploaded file by its opaque ID. Requires authentication.
672672

673673
---
674674

675+
### Git
676+
677+
#### `GET /api/sessions/:id/git/status`
678+
679+
Returns parsed git status for a session's working directory.
680+
681+
**Response (200):**
682+
683+
```json
684+
{
685+
"branch": "main",
686+
"ahead": 3,
687+
"behind": 0,
688+
"staged": [{ "path": "src/index.js", "status": "M", "oldPath": null }],
689+
"modified": [{ "path": "README.md", "status": "M", "oldPath": null }],
690+
"untracked": ["new-file.txt"],
691+
"isGitRepo": true
692+
}
693+
```
694+
695+
| Field | Type | Description |
696+
| ----------- | ------- | ----------------------------------------------------------------------- |
697+
| `branch` | string | Current branch name |
698+
| `ahead` | number | Commits ahead of upstream |
699+
| `behind` | number | Commits behind upstream |
700+
| `staged` | array | Staged files, each with `path`, `status`, and `oldPath` (for renames) |
701+
| `modified` | array | Modified files, each with `path`, `status`, and `oldPath` (for renames) |
702+
| `untracked` | array | Untracked file paths |
703+
| `isGitRepo` | boolean | Whether the session's cwd is inside a git repository |
704+
705+
---
706+
707+
#### `GET /api/sessions/:id/git/diff`
708+
709+
Returns file diff for a specific file.
710+
711+
**Query parameters:**
712+
713+
| Parameter | Type | Description |
714+
| ----------- | ------- | -------------------------------------------- |
715+
| `file` | string | File path relative to repo root (required) |
716+
| `staged` | boolean | Show staged changes instead of working tree |
717+
| `untracked` | boolean | Treat file as untracked (diff against empty) |
718+
| `context` | number | Number of context lines around changes |
719+
720+
**Response (200):**
721+
722+
```json
723+
{
724+
"file": "src/index.js",
725+
"hunks": [
726+
{
727+
"header": "@@ -10,6 +10,7 @@",
728+
"oldStart": 10,
729+
"oldLines": 6,
730+
"newStart": 10,
731+
"newLines": 7,
732+
"lines": [
733+
{
734+
"type": "context",
735+
"content": "const express = require('express');",
736+
"oldLine": 10,
737+
"newLine": 10
738+
},
739+
{
740+
"type": "add",
741+
"content": "const cors = require('cors');",
742+
"oldLine": null,
743+
"newLine": 11
744+
}
745+
]
746+
}
747+
],
748+
"additions": 1,
749+
"deletions": 0,
750+
"isBinary": false
751+
}
752+
```
753+
754+
| Field | Type | Description |
755+
| ----------- | ------- | ----------------------------------------------- |
756+
| `file` | string | File path |
757+
| `hunks` | array | Diff hunks with header, line ranges, and lines |
758+
| `additions` | number | Total number of added lines |
759+
| `deletions` | number | Total number of deleted lines |
760+
| `isBinary` | boolean | Whether the file is binary (no line-level diff) |
761+
762+
Each line in a hunk contains `type` (`"add"`, `"remove"`, or `"context"`), `content`, `oldLine`, and `newLine`.
763+
764+
---
765+
766+
#### `GET /api/sessions/:id/git/blame`
767+
768+
Returns per-line blame information for a file.
769+
770+
**Query parameters:**
771+
772+
| Parameter | Type | Description |
773+
| --------- | ------ | ------------------------------------------ |
774+
| `file` | string | File path relative to repo root (required) |
775+
776+
**Response (200):**
777+
778+
```json
779+
{
780+
"file": "src/index.js",
781+
"lines": [
782+
{
783+
"line": 1,
784+
"content": "const express = require('express');",
785+
"commit": "a1b2c3d",
786+
"author": "Jane Doe",
787+
"date": "2025-01-15T10:30:00.000Z",
788+
"summary": "Initial commit"
789+
}
790+
]
791+
}
792+
```
793+
794+
| Field | Type | Description |
795+
| ------- | ------ | ---------------------- |
796+
| `file` | string | File path |
797+
| `lines` | array | Per-line blame entries |
798+
799+
Each line entry contains `line` (number), `content` (string), `commit` (short hash), `author`, `date` (ISO 8601), and `summary` (commit message first line).
800+
801+
---
802+
803+
#### `GET /api/sessions/:id/git/log`
804+
805+
Returns commit log for the repository.
806+
807+
**Query parameters:**
808+
809+
| Parameter | Type | Description |
810+
| --------- | ------ | ----------------------------------------------- |
811+
| `limit` | number | Max commits to return (default 20, max 100) |
812+
| `file` | string | Filter commits to those touching this file path |
813+
814+
**Response (200):**
815+
816+
```json
817+
{
818+
"commits": [
819+
{
820+
"hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
821+
"shortHash": "a1b2c3d",
822+
"author": "Jane Doe",
823+
"email": "jane@example.com",
824+
"date": "2025-01-15T10:30:00.000Z",
825+
"subject": "feat: add git integration",
826+
"body": ""
827+
}
828+
]
829+
}
830+
```
831+
832+
| Field | Type | Description |
833+
| --------- | ----- | --------------- |
834+
| `commits` | array | List of commits |
835+
836+
Each commit contains `hash`, `shortHash`, `author`, `email`, `date` (ISO 8601), `subject`, and `body`.
837+
838+
---
839+
840+
### Push Notifications
841+
842+
#### `GET /api/push/vapid-key`
843+
844+
Returns the VAPID public key needed to create a push subscription on the client.
845+
846+
**Response (200):**
847+
848+
```json
849+
{ "publicKey": "BNq..." }
850+
```
851+
852+
| Field | Type | Description |
853+
| ----------- | ------ | ---------------------------------- |
854+
| `publicKey` | string | Base64url-encoded VAPID public key |
855+
856+
---
857+
858+
#### `POST /api/push/subscribe`
859+
860+
Register a Web Push subscription with the server.
861+
862+
**Request:**
863+
864+
```json
865+
{
866+
"subscription": {
867+
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
868+
"keys": {
869+
"p256dh": "BNq...",
870+
"auth": "abc..."
871+
}
872+
}
873+
}
874+
```
875+
876+
**Response (200):**
877+
878+
```json
879+
{ "ok": true }
880+
```
881+
882+
---
883+
884+
#### `DELETE /api/push/unsubscribe`
885+
886+
Remove a previously registered push subscription.
887+
888+
**Request:**
889+
890+
```json
891+
{ "endpoint": "https://fcm.googleapis.com/fcm/send/..." }
892+
```
893+
894+
**Response (200):**
895+
896+
```json
897+
{ "ok": true }
898+
```
899+
900+
---
901+
675902
### Port Preview
676903

677904
#### `GET /preview/:port/*`
@@ -793,6 +1020,25 @@ The server validates resize dimensions: `cols` must be between 1–500 and `rows
7931020
{ "type": "exit", "code": 0 }
7941021
```
7951022

1023+
#### Notification
1024+
1025+
Sent when a command completes (child process exits). Broadcast in real time to connected clients and replayed on attach for events that occurred while disconnected.
1026+
1027+
```json
1028+
{
1029+
"type": "notification",
1030+
"notificationType": "command-complete",
1031+
"sessionName": "my-project",
1032+
"timestamp": 1719849600000
1033+
}
1034+
```
1035+
1036+
| Field | Type | Description |
1037+
| ------------------ | ------ | ------------------------------------------- |
1038+
| `notificationType` | string | Notification kind (`command-complete`) |
1039+
| `sessionName` | string | Name of the session where the event fired |
1040+
| `timestamp` | number | Unix timestamp (ms) when the event occurred |
1041+
7961042
#### Error
7971043

7981044
```json

docs/architecture.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ termbeam/
1313
│ │ ├── auth.js # Authentication & rate limiting
1414
│ │ ├── websocket.js # WebSocket connection handling
1515
│ │ ├── sessions.js # PTY session management
16-
│ │ └── preview.js # Port preview reverse proxy
16+
│ │ ├── preview.js # Port preview reverse proxy
17+
│ │ └── push.js # Web Push notification manager
1718
│ ├── cli/ # CLI subcommands & tools
1819
│ │ ├── index.js # Argument parsing & help
1920
│ │ ├── client.js # WebSocket terminal client (resume)
@@ -29,19 +30,20 @@ termbeam/
2930
│ │ ├── shells.js # Shell detection (cross-platform)
3031
│ │ ├── git.js # Git repo detection & status
3132
│ │ ├── version.js # Smart version detection
32-
│ │ └── update-check.js # npm update checking
33+
│ │ ├── update-check.js # npm update checking
34+
│ │ └── vapid.js # VAPID key generation & persistence
3335
│ └── frontend/ # React 19 + Vite + TypeScript SPA
3436
│ ├── src/
3537
│ │ ├── App.tsx # Root component
3638
│ │ ├── main.tsx # Entry point
3739
│ │ ├── components/ # UI components
3840
│ │ ├── hooks/ # Custom React hooks
39-
│ │ ├── services/ # API & WebSocket clients
41+
│ │ ├── services/ # API, WebSocket & push subscription clients
4042
│ │ ├── stores/ # Zustand state stores
4143
│ │ ├── styles/ # CSS stylesheets
4244
│ │ ├── themes/ # Terminal themes
4345
│ │ ├── types/ # TypeScript type definitions
44-
│ │ └── sw.ts # Service worker source
46+
│ │ └── sw.ts # Service worker (caching + push notifications)
4547
│ ├── package.json
4648
│ ├── vite.config.ts
4749
│ └── tsconfig.json
@@ -77,7 +79,7 @@ Factory function `createAuth(password)` returns an object with middleware, token
7779

7880
### `server/sessions.js` — Session Manager
7981

80-
`SessionManager` class wraps the PTY lifecycle. Handles spawning, tracking, listing, updating, and cleaning up terminal sessions. Each session has an auto-assigned color, tracks `lastActivity` timestamps, a `createdAt` timestamp, and supports live updates via the `update()` method. Sessions maintain a scrollback buffer with a high/low-water mark (trimmed back to ~500k characters when it grows beyond 1,000,000 characters) that is sent to newly connecting clients, and track a `clients` Set of active WebSocket connections. Supports an optional `initialCommand` that is written to the PTY shortly after spawn. The `list()` method detects the live working directory of the shell process (via `lsof` on macOS, `/proc` on Linux) and enriches each session with git repository information, using an async cache to avoid blocking the event loop.
82+
`SessionManager` class wraps the PTY lifecycle. Handles spawning, tracking, listing, updating, and cleaning up terminal sessions. Each session has an auto-assigned color, tracks `lastActivity` timestamps, a `createdAt` timestamp, and supports live updates via the `update()` method. Sessions maintain a scrollback buffer with a high/low-water mark (trimmed back to ~500k characters when it grows beyond 1,000,000 characters) that is sent to newly connecting clients, and track a `clients` Set of active WebSocket connections. Supports an optional `initialCommand` that is written to the PTY shortly after spawn. The `list()` method detects the live working directory of the shell process (via `lsof` on macOS, `/proc` on Linux) and enriches each session with git repository information, using an async cache to avoid blocking the event loop. Includes a process-tree monitor that polls for child process exits every 2 seconds (via `ps` + `awk` to count descendant processes), enabling command-completion detection for push notifications.
8183

8284
### `utils/git.js` — Git Repository Detection
8385

@@ -89,7 +91,11 @@ Registers all Express routes: login page (`GET /login`), auth API, session CRUD
8991

9092
### `server/websocket.js` — WebSocket Handler
9193

92-
Handles real-time communication: validates the Origin header to reject cross-origin connections, WebSocket-level authentication (password or token), session attachment, terminal I/O forwarding, and resize events. When multiple clients are connected to the same session, the PTY is resized to the minimum dimensions across active clients (active within the last 60 seconds). Idle clients are excluded from the size calculation so that a backgrounded phone tab does not constrain the terminal when resuming from a laptop. Sends keepalive pings every 30 seconds to help mobile browsers maintain the WebSocket and to surface broken connections sooner at the transport level.
94+
Handles real-time communication: validates the Origin header to reject cross-origin connections, WebSocket-level authentication (password or token), session attachment, terminal I/O forwarding, and resize events. When multiple clients are connected to the same session, the PTY is resized to the minimum dimensions across active clients (active within the last 60 seconds). Idle clients are excluded from the size calculation so that a backgrounded phone tab does not constrain the terminal when resuming from a laptop. Sends keepalive pings every 15 seconds and terminates connections that do not reply with a pong.
95+
96+
### `server/push.js` — Push Notification Manager
97+
98+
`PushManager` class for Web Push notifications. Manages VAPID authentication, push subscriptions (in-memory), and notification delivery via the `web-push` npm package. Exposes methods for subscribing/unsubscribing clients and sending notifications when commands complete in a session.
9399

94100
### `server/preview.js` — Port Preview Proxy
95101

@@ -123,6 +129,10 @@ Runs a step-by-step terminal wizard (in an alternate screen buffer) that walks t
123129

124130
Provides ANSI color helpers (`green`, `yellow`, `red`, `cyan`, `bold`, `dim`) and interactive prompt functions (`ask`, `choose`, `confirm`, `createRL`). Extracted from `service.js` so both the service install wizard and the interactive setup wizard can share the same prompt primitives.
125131

132+
### `utils/vapid.js` — VAPID Key Management
133+
134+
Generates and persists VAPID key pairs for Web Push authentication. Keys are stored in `~/.termbeam/vapid.json` and reused across server restarts so that existing push subscriptions remain valid.
135+
126136
### `utils/update-check.js` — Update Checker
127137

128138
Checks the npm registry for newer versions of TermBeam. Fetches the latest published version from `registry.npmjs.org`, compares it against the running version using semver comparison (`isNewerVersion`), and caches the result for 24 hours in `~/.termbeam/update-check.json` to avoid repeated network requests. Includes `sanitizeVersion()` to strip ANSI escape sequences and control characters from registry responses (terminal injection protection). Also provides `detectInstallMethod()` which inspects environment variables to determine whether TermBeam was installed via npm, npx, yarn, or pnpm, returning the appropriate upgrade command.
@@ -139,6 +149,8 @@ The terminal page includes several client-side features:
139149

140150
- **Terminal search** — <kbd>Ctrl+F</kbd> / <kbd>Cmd+F</kbd> opens a search bar overlay powered by the xterm.js `SearchAddon`. Supports regex matching with next/previous navigation.
141151
- **Command completion notifications** — uses the browser Notification API to alert when a command finishes in a background tab. Toggled via a bell icon; preference stored in `localStorage` (`termbeam-notifications`).
152+
- **Push notifications** — native push notifications via the Web Push API, delivered even when the browser tab is closed. The service worker (`sw.ts`) handles push events and uses the Badge API to show unread counts. Push subscription lifecycle (subscribe, unsubscribe, VAPID key mismatch detection) is managed by `services/pushSubscription.ts`.
153+
- **Git changes view**`GitChanges/`, `DiffViewer/`, and `BlameGutter/` components in the CodeViewer directory provide a full git integration UI: staged/unstaged diffs with syntax highlighting, per-line blame annotations, and commit history browsing.
142154
- **Command palette** — <kbd>Ctrl+K</kbd> / <kbd>Cmd+K</kbd> (or the floating ⚙️ button) opens a slide-out tool panel with categorized actions (Session, Search, View, Share, Notifications, System).
143155

144156
## Data Flow
@@ -158,7 +170,8 @@ Client (Phone Browser)
158170
├─ attach ├─ spawn shell
159171
├─ input ──────► ├─ write stdin
160172
├─ resize ├─ resize terminal
161-
└─ output ◄────── └─ read stdout
173+
├─ output ◄────── └─ read stdout
174+
└─ notification ──► Push Manager ──► Web Push
162175
```
163176

164177
### `client.js` — WebSocket Terminal Client

0 commit comments

Comments
 (0)