Skip to content

Commit 63e5058

Browse files
committed
2 parents f7d3970 + 913c3f1 commit 63e5058

71 files changed

Lines changed: 13412 additions & 587 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ frontend/node_modules/
1414
# Build output from src/frontend/ (rebuilt by prepublishOnly)
1515
public/
1616
*.tsbuildinfo
17+
.playwright-mcp/
18+
test-diff.md

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,22 @@ flowchart LR
121121

122122
For all flags, subcommands, and environment variables, see the [Configuration docs](https://dorlugasigal.github.io/TermBeam/configuration/).
123123

124+
## Background Service
125+
126+
Run TermBeam as an always-on background service using the built-in PM2 integration:
127+
128+
```bash
129+
termbeam service install # interactive wizard — configures PM2, password, tunnel, boot auto-start
130+
termbeam service status # check process info, uptime, memory
131+
termbeam service logs # tail live logs
132+
termbeam service restart # restart after config changes
133+
termbeam service uninstall # stop and remove the service
134+
```
135+
136+
The installer checks for [PM2](https://pm2.keymetrics.io/) (and offers to install it), then walks you through password, port, access mode, and boot auto-start options. Config is saved to `~/.termbeam/ecosystem.config.js`.
137+
138+
For systemd, launchd, and Windows Task Scheduler setup, see the [Running in Background docs](https://dorlugasigal.github.io/TermBeam/running-in-background/).
139+
124140
## Security
125141

126142
TermBeam auto-generates a password and creates a secure tunnel by default, binding to `127.0.0.1` (localhost only). Auth uses httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, QR codes contain single-use share tokens (5-min expiry), and security headers (X-Frame-Options, CSP, nosniff) are set on all responses.

package-lock.json

Lines changed: 146 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/landing/scripts/record-showcase.cjs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#!/usr/bin/env node
22
'use strict';
33

4+
const path = require('path');
45
const { chromium } = require(
5-
require.resolve('playwright', { paths: ['/Users/dorlugasigal/Projects/termbeam'] })
6+
require.resolve('playwright', { paths: [path.resolve(__dirname, '../../..')] }),
67
);
78
const { execSync } = require('child_process');
89
const http = require('http');
910
const fs = require('fs');
10-
const path = require('path');
1111

1212
const BASE_URL = 'http://localhost:3456';
1313
const FFMPEG = '/opt/homebrew/bin/ffmpeg';
@@ -107,12 +107,12 @@ async function saveVideo(capture, outputName) {
107107
`"${FFMPEG}" -y -framerate ${CAPTURE_FPS} -i "${capture.dir}/frame-%05d.png" ` +
108108
`-c:v libx264 -crf 22 -preset slow -r ${OUTPUT_FPS} ` +
109109
`-an -movflags +faststart -pix_fmt yuv420p "${mp4Path}"`,
110-
{ stdio: 'pipe' }
110+
{ stdio: 'pipe' },
111111
);
112112
fs.rmSync(capture.dir, { recursive: true, force: true });
113113
const stat = fs.statSync(mp4Path);
114114
const dur = execSync(
115-
`"${FFMPEG}" -i "${mp4Path}" 2>&1 | grep Duration | sed 's/.*Duration: //' | sed 's/,.*//'`
115+
`"${FFMPEG}" -i "${mp4Path}" 2>&1 | grep Duration | sed 's/.*Duration: //' | sed 's/,.*//'`,
116116
)
117117
.toString()
118118
.trim();
@@ -126,11 +126,15 @@ async function focusTerminal(page, timeout = 5000) {
126126
const rows = document.querySelector('.xterm-rows');
127127
return rows && rows.textContent && rows.textContent.trim().length > 5;
128128
},
129-
{ timeout }
129+
{ timeout },
130130
)
131131
.catch(() => {});
132132
await page.waitForTimeout(300);
133-
await page.locator('.xterm-screen').first().click().catch(() => {});
133+
await page
134+
.locator('.xterm-screen')
135+
.first()
136+
.click()
137+
.catch(() => {});
134138
await page.waitForTimeout(150);
135139
}
136140

@@ -194,9 +198,9 @@ async function recordAgentsDesktop(browser) {
194198
const btn = page.locator('[data-testid="hub-new-session-btn"]');
195199
await btn.waitFor({ timeout: 5000 }).catch(() => {});
196200
await btn.click().catch(() => {});
197-
await page.waitForSelector('[data-testid="new-session-modal"]', { timeout: 5000 }).catch(
198-
() => {}
199-
);
201+
await page
202+
.waitForSelector('[data-testid="new-session-modal"]', { timeout: 5000 })
203+
.catch(() => {});
200204
// Let user see the modal with agent cards
201205
await page.waitForTimeout(2500);
202206
// Look for Claude Code agent card and click it
@@ -444,7 +448,10 @@ async function recordResumeDesktop(browser) {
444448
const resumeCards = page.locator('[class*="card"]').filter({ hasText: /.+/ });
445449
const cardCount = await resumeCards.count();
446450
if (cardCount > 0) {
447-
await resumeCards.first().click().catch(() => {});
451+
await resumeCards
452+
.first()
453+
.click()
454+
.catch(() => {});
448455
await page.waitForTimeout(1500);
449456
} else {
450457
await page.waitForTimeout(1500);
@@ -485,14 +492,18 @@ async function recordGitdiffDesktop(browser) {
485492
await paletteTrigger.click().catch(() => {});
486493
await page.waitForTimeout(800);
487494
// Click "Git changes" action
488-
const gitAction = page.locator('[data-testid="palette-action"]:has-text("Git changes")').first();
495+
const gitAction = page
496+
.locator('[data-testid="palette-action"]:has-text("Git changes")')
497+
.first();
489498
const hasGitAction = await gitAction.count();
490499
if (hasGitAction > 0) {
491500
await gitAction.click().catch(() => {});
492501
await page.waitForTimeout(1500);
493502
// Code viewer opens with git changes panel — click a non-binary file
494503
// Look for files with .js, .ts, .json, .css, .md extensions
495-
const fileButtons = page.locator('button[aria-label*=".js"], button[aria-label*=".ts"], button[aria-label*=".json"], button[aria-label*=".css"], button[aria-label*=".md"], button[aria-label*=".cjs"]');
504+
const fileButtons = page.locator(
505+
'button[aria-label*=".js"], button[aria-label*=".ts"], button[aria-label*=".json"], button[aria-label*=".css"], button[aria-label*=".md"], button[aria-label*=".cjs"]',
506+
);
496507
const fileCount = await fileButtons.count();
497508
if (fileCount > 0) {
498509
await fileButtons.first().click();
@@ -507,7 +518,9 @@ async function recordGitdiffDesktop(browser) {
507518
}
508519
} else {
509520
// Fallback: open "View code" instead
510-
const codeAction = page.locator('[data-testid="palette-action"]:has-text("View code")').first();
521+
const codeAction = page
522+
.locator('[data-testid="palette-action"]:has-text("View code")')
523+
.first();
511524
if ((await codeAction.count()) > 0) {
512525
await codeAction.click().catch(() => {});
513526
await page.waitForTimeout(1500);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { AgentMessage as AgentMessageType, AgentStatus } from '@/stores/agentStore';
2+
import { useAgentStore } from '@/stores/agentStore';
3+
import { AgentMessage } from './AgentMessage';
4+
import { AgentThinking } from './AgentThinking';
5+
import styles from './AgentView.module.css';
6+
7+
interface AgentChatPanelProps {
8+
messages: AgentMessageType[];
9+
status: AgentStatus;
10+
thinkingStartTime: number | null;
11+
scrollRef: React.RefObject<HTMLDivElement | null>;
12+
}
13+
14+
const isThinkingOrWorking = (s: AgentStatus): s is 'thinking' | 'working' =>
15+
s === 'thinking' || s === 'working';
16+
17+
export function AgentChatPanel({
18+
messages,
19+
status,
20+
thinkingStartTime,
21+
scrollRef,
22+
}: AgentChatPanelProps) {
23+
const toggleToolCallCollapse = useAgentStore((s) => s.toggleToolCallCollapse);
24+
const isThinking = isThinkingOrWorking(status);
25+
26+
if (messages.length === 0 && !isThinking) {
27+
return (
28+
<div className={styles.chatPanel}>
29+
<div className={styles.emptyState}>
30+
<span className={styles.emptyIcon}>💬</span>
31+
<span className={styles.emptyTitle}>Start a conversation with your agent</span>
32+
<span className={styles.emptyHint}>
33+
Type a message below to get started. The agent can edit files, run commands, and answer
34+
questions about your project.
35+
</span>
36+
</div>
37+
<div ref={scrollRef} className={styles.scrollAnchor} />
38+
</div>
39+
);
40+
}
41+
42+
return (
43+
<div className={styles.chatPanel}>
44+
<div className={styles.chatPanelInner}>
45+
<div className={styles.messageList}>
46+
{messages.map((msg) => (
47+
<AgentMessage
48+
key={msg.id}
49+
message={msg}
50+
onToggleToolCallCollapse={(tcId) => toggleToolCallCollapse(msg.id, tcId)}
51+
/>
52+
))}
53+
{isThinkingOrWorking(status) && (
54+
<AgentThinking startTime={thinkingStartTime} status={status} />
55+
)}
56+
</div>
57+
</div>
58+
<div ref={scrollRef} className={styles.scrollAnchor} />
59+
</div>
60+
);
61+
}

0 commit comments

Comments
 (0)