feat: add agent workflow messaging and skill

This commit is contained in:
xixu-me committed 2026-03-21 22:02:14 +08:00
1 parent b451771169
commit ee82019d0b
30 files changed
+1462 -87

No files matched your search

+6 -6
View File
@@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server."
content="Xdrop is an open source encrypted file transfer app for browsers and agent-driven terminal workflows, keeping plaintext file names, contents, and keys off the server."
/>
<meta
name="robots"
@@ -20,26 +20,26 @@
<meta property="og:type" content="website" />
<meta
property="og:title"
content="Open Source Encrypted File Transfer in the Browser | Xdrop"
content="Open Source Encrypted File Transfer for Browsers and Agents | Xdrop"
/>
<meta
property="og:description"
content="Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server."
content="Xdrop is an open source encrypted file transfer app for browsers and agent-driven terminal workflows, keeping plaintext file names, contents, and keys off the server."
/>
<meta property="og:image" content="/brand-lockup-horizontal.png" />
<meta property="og:image:alt" content="Xdrop horizontal brand lockup" />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Open Source Encrypted File Transfer in the Browser | Xdrop"
content="Open Source Encrypted File Transfer for Browsers and Agents | Xdrop"
/>
<meta
name="twitter:description"
content="Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server."
content="Xdrop is an open source encrypted file transfer app for browsers and agent-driven terminal workflows, keeping plaintext file names, contents, and keys off the server."
/>
<meta name="twitter:image" content="/brand-lockup-horizontal.png" />
<meta name="theme-color" content="#12140f" />
<title>Open Source Encrypted File Transfer in the Browser | Xdrop</title>
<title>Open Source Encrypted File Transfer for Browsers and Agents | Xdrop</title>
</head>
<body>
<div id="root"></div>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "Xdrop",
"short_name": "Xdrop",
"description": "Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server.",
"description": "Xdrop is an open source encrypted file transfer app for browsers and agent-driven terminal workflows, keeping plaintext file names, contents, and keys off the server.",
"theme_color": "#12140f",
"background_color": "#f7f2e8",
"display": "standalone",
+22 -14
View File
@@ -18,9 +18,21 @@ const DEFAULT_ROBOTS =
const PRIVATE_ROBOTS = 'noindex, nofollow, noarchive, nosnippet'
const STRUCTURED_DATA_ID = 'xdrop-structured-data-static'
const HOME_TITLE = 'Open Source Encrypted File Transfer in the Browser | Xdrop'
const HOME_TITLE = 'Open Source Encrypted File Transfer for Browsers and Agents | Xdrop'
const HOME_DESCRIPTION =
'Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server.'
'Xdrop is an open source encrypted file transfer app for browsers and agent-driven terminal workflows, keeping plaintext file names, contents, and keys off the server.'
const HISTORY_PAGE_TITLE = 'Manage Transfers on This Device | Xdrop'
const HISTORY_PAGE_DESCRIPTION =
'Manage encrypted transfers stored in this browser on this device. There is no account or cross-device history.'
const SHARE_PAGE_TITLE = 'Share the Full Link | Xdrop'
const SHARE_PAGE_DESCRIPTION =
'Review upload status and copy the full share link for a transfer staged in this browser on this device.'
const RECEIVE_PAGE_TITLE = 'Download and Decrypt in the Browser | Xdrop'
const RECEIVE_PAGE_DESCRIPTION =
'Download files from this transfer and decrypt them in the browser. The decryption key stays in the share link fragment.'
const NOT_FOUND_PAGE_TITLE = 'Page Not Found | Xdrop'
const NOT_FOUND_PAGE_DESCRIPTION =
'The address does not map to a page in Xdrop. If this came from a shared transfer, ask for the full URL, including the #k=... decryption fragment.'
const routeShells = [
{
@@ -34,35 +46,31 @@ const routeShells = [
{
outputPath: 'transfers/index.html',
pagePath: '/transfers',
title: 'Manage Transfers on This Device | Xdrop',
description:
'Manage encrypted transfers stored in this browser. There is no account or cross-device history.',
title: HISTORY_PAGE_TITLE,
description: HISTORY_PAGE_DESCRIPTION,
robots: PRIVATE_ROBOTS,
exposeUrl: false,
},
{
outputPath: 'share/index.html',
pagePath: '/share/',
title: 'Share the Full Link | Xdrop',
description:
'Review upload status and copy the full share link for a browser-encrypted transfer.',
title: SHARE_PAGE_TITLE,
description: SHARE_PAGE_DESCRIPTION,
robots: PRIVATE_ROBOTS,
exposeUrl: false,
},
{
outputPath: 't/index.html',
pagePath: '/t/',
title: 'Download and Decrypt in the Browser | Xdrop',
description:
'Download files from this transfer and decrypt them in the browser. The decryption key stays in the share link fragment.',
title: RECEIVE_PAGE_TITLE,
description: RECEIVE_PAGE_DESCRIPTION,
robots: PRIVATE_ROBOTS,
exposeUrl: false,
},
{
outputPath: 'not-found/index.html',
title: 'Page Not Found | Xdrop',
description:
'The address does not map to a page in Xdrop. If this came from a shared transfer, ask for the full URL, including the #k=... decryption fragment.',
title: NOT_FOUND_PAGE_TITLE,
description: NOT_FOUND_PAGE_DESCRIPTION,
robots: PRIVATE_ROBOTS,
exposeUrl: false,
},
+1 -1
View File
@@ -20,7 +20,7 @@ export function Shell() {
/>
<span className="brand-copy">
<span className="brand-mark">Xdrop</span>
<span className="brand-note">Browser-encrypted transfer</span>
<span className="brand-note">Encrypted file transfer</span>
</span>
</NavLink>
<nav className="nav">
+3 -4
View File
@@ -1,13 +1,12 @@
import { HistoryBoard } from '@/features/history/HistoryBoard'
import { PRIVATE_ROBOTS } from '@/lib/seo/site'
import { HISTORY_PAGE_DESCRIPTION, HISTORY_PAGE_TITLE, PRIVATE_ROBOTS } from '@/lib/seo/site'
import { usePageMetadata } from '@/lib/seo/usePageMetadata'
/** HistoryPage hides local-only transfer history from search engines. */
export function HistoryPage() {
usePageMetadata({
title: 'Manage Transfers on This Device | Xdrop',
description:
'Manage encrypted transfers stored in this browser. There is no account or cross-device history.',
title: HISTORY_PAGE_TITLE,
description: HISTORY_PAGE_DESCRIPTION,
robots: PRIVATE_ROBOTS,
exposeUrl: false,
})
+1 -1
View File
@@ -27,7 +27,7 @@ describe('HomePage', () => {
expect(usePageMetadataMock).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Open Source Encrypted File Transfer in the Browser | Xdrop',
title: 'Open Source Encrypted File Transfer for Browsers and Agents | Xdrop',
structuredData: expect.arrayContaining([
expect.objectContaining({
'@type': 'Organization',
@@ -14,6 +14,8 @@ describe('NotFoundPage', () => {
expect(screen.getByRole('heading', { name: 'This page was not found.' })).toBeInTheDocument()
expect(screen.getByRole('heading', { name: 'Try these checks' })).toBeInTheDocument()
expect(screen.getByText('Try the browser that created the transfer')).toBeInTheDocument()
expect(
screen.getByText('Try the device and browser that created the transfer'),
).toBeInTheDocument()
})
})
+6 -7
View File
@@ -1,13 +1,12 @@
import { Card } from '@/components/ui/Card'
import { PRIVATE_ROBOTS } from '@/lib/seo/site'
import { NOT_FOUND_PAGE_DESCRIPTION, NOT_FOUND_PAGE_TITLE, PRIVATE_ROBOTS } from '@/lib/seo/site'
import { usePageMetadata } from '@/lib/seo/usePageMetadata'
/** NotFoundPage explains the most common ways secure share links break. */
export function NotFoundPage() {
usePageMetadata({
title: 'Page Not Found | Xdrop',
description:
'The address does not map to a page in Xdrop. If this came from a shared transfer, ask for the full URL, including the #k=... decryption fragment.',
title: NOT_FOUND_PAGE_TITLE,
description: NOT_FOUND_PAGE_DESCRIPTION,
robots: PRIVATE_ROBOTS,
exposeUrl: false,
})
@@ -37,10 +36,10 @@ export function NotFoundPage() {
<ul className="file-list">
<li className="file-row not-found-tip">
<div className="file-stack">
<strong>Try the browser that created the transfer</strong>
<strong>Try the device and browser that created the transfer</strong>
<p className="muted">
Sender history and local transfer controls only exist on the device that created the
transfer.
Sender history and local transfer controls only exist in the browser that created
the transfer on that device.
</p>
</div>
</li>
+3 -4
View File
@@ -2,7 +2,7 @@ import { useParams } from 'react-router-dom'
import { ShareCard } from '@/features/share/ShareCard'
import { useTransfers } from '@/features/upload/TransferContext'
import { PRIVATE_ROBOTS } from '@/lib/seo/site'
import { PRIVATE_ROBOTS, SHARE_PAGE_DESCRIPTION, SHARE_PAGE_TITLE } from '@/lib/seo/site'
import { usePageMetadata } from '@/lib/seo/usePageMetadata'
/** SharePage shows sender-side progress and sharing controls for one local transfer. */
@@ -12,9 +12,8 @@ export function SharePage() {
const transfer = transfers.find((item) => item.id === transferId)
usePageMetadata({
title: 'Share the Full Link | Xdrop',
description:
'Review upload status and copy the full share link for a browser-encrypted transfer.',
title: SHARE_PAGE_TITLE,
description: SHARE_PAGE_DESCRIPTION,
robots: PRIVATE_ROBOTS,
exposeUrl: false,
})
@@ -198,7 +198,7 @@ describe('HistoryBoard', () => {
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
expect(
screen.getByText(/Confirm delete to remove this transfer from storage and from this device/i),
screen.getByText(/Confirm delete to remove this transfer from this device/i),
).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Confirm delete' }))
@@ -235,7 +235,7 @@ export function HistoryBoard() {
{pendingDeleteTransferId === transfer.id ? (
<p aria-live="polite" className="warning">
{canManageTransfer
? 'Confirm delete to remove this transfer from storage and from this device.'
? 'Confirm delete to remove this transfer from this device.'
: 'Confirm forget to remove this local record from this device.'}
</p>
) : null}
@@ -17,7 +17,7 @@ import { formatBytes } from '@/lib/files/formatBytes'
import { safeDownloadName, sanitizePath } from '@/lib/files/paths'
import { isAbortError, openSaveWritable, saveBlob, supportsStreamingSave } from '@/lib/files/save'
import { formatLocalDateTime } from '@/lib/i18n/formatDateTime'
import { PRIVATE_ROBOTS } from '@/lib/seo/site'
import { PRIVATE_ROBOTS, RECEIVE_PAGE_DESCRIPTION, RECEIVE_PAGE_TITLE } from '@/lib/seo/site'
import { usePageMetadata } from '@/lib/seo/usePageMetadata'
type Props = {
@@ -49,9 +49,8 @@ export function ReceiveTransfer({ transferId }: Props) {
activeDownload !== null || downloadProgress > 0 || Boolean(downloadError)
usePageMetadata({
title: 'Download and Decrypt in the Browser | Xdrop',
description:
'Download files from this transfer and decrypt them in the browser. The decryption key stays in the share link fragment.',
title: RECEIVE_PAGE_TITLE,
description: RECEIVE_PAGE_DESCRIPTION,
robots: PRIVATE_ROBOTS,
exposeUrl: false,
})
@@ -109,7 +109,9 @@ describe('ShareCard', () => {
)
expect(screen.getByText('Transfer not on this device')).toBeInTheDocument()
expect(screen.getByText('Open it in the browser that created it.')).toBeInTheDocument()
expect(
screen.getByText('Open it in the same browser on the device that created it.'),
).toBeInTheDocument()
})
it('copies the full link and resets the copied state', async () => {
@@ -310,7 +312,9 @@ describe('ShareCard', () => {
expect(screen.getByText('25% uploaded')).toBeInTheDocument()
expect(
screen.getByText('Upload will continue automatically when this browser returns.'),
screen.getByText(
'Upload will continue automatically when you return here in the same browser on this device.',
),
).toBeInTheDocument()
expect(screen.getByText(/Privacy mode removed local transfer controls/i)).toBeInTheDocument()
@@ -322,7 +326,7 @@ describe('ShareCard', () => {
await act(async () => {
await Promise.resolve()
})
expect(screen.getByText('Upload stopped in this browser.')).toBeInTheDocument()
expect(screen.getByText('Upload stopped in this browser on this device.')).toBeInTheDocument()
rerender(
<MemoryRouter>
+3 -3
View File
@@ -115,7 +115,7 @@ export function ShareCard({ transfer }: Props) {
<PageStateCard
eyebrow="Share"
title="Transfer not on this device"
body="Open it in the browser that created it."
body="Open it in the same browser on the device that created it."
/>
)
}
@@ -260,9 +260,9 @@ function shareStatusCopy(status: LocalTransferRecord['status']) {
case 'ready':
return undefined
case 'paused':
return 'Upload will continue automatically when this browser returns.'
return 'Upload will continue automatically when you return here in the same browser on this device.'
case 'failed':
return 'Upload stopped in this browser.'
return 'Upload stopped in this browser on this device.'
case 'deleted':
return 'This transfer was deleted.'
default:
@@ -549,7 +549,7 @@ describe('TransferProvider actions', () => {
await waitFor(() => {
expect(
screen.getByText(
't1:paused:This page was closed or refreshed. Upload will continue automatically when this browser returns.',
't1:paused:This page was closed or refreshed. Upload will continue automatically when you return here in the same browser on this device.',
),
).toBeInTheDocument()
})
@@ -360,7 +360,7 @@ describe('TransferContext helpers', () => {
makeTransferRecord('paused', {
id: 't2',
lastError:
'This page was closed or refreshed. Upload will continue automatically when this browser returns.',
'This page was closed or refreshed. Upload will continue automatically when you return here in the same browser on this device.',
}),
makeTransferRecord('ready', { id: 't3' }),
])
@@ -370,7 +370,7 @@ describe('TransferContext helpers', () => {
expect.objectContaining({
id: 't1',
lastError:
'This page was closed or refreshed. Upload will continue automatically when this browser returns.',
'This page was closed or refreshed. Upload will continue automatically when you return here in the same browser on this device.',
status: 'paused',
}),
)
@@ -226,7 +226,7 @@ describe('TransferProvider', () => {
't1',
createTransferRecord('paused', {
lastError:
'This page was closed or refreshed. Upload will continue automatically when this browser returns.',
'This page was closed or refreshed. Upload will continue automatically when you return here in the same browser on this device.',
}),
)
sourcesStore.set('t1:file-1', createSourceRecord())
@@ -74,7 +74,7 @@ type UploadSource = PersistedSourceRecord & {
/** This message marks transfers that should resume automatically after a page return. */
const RESUME_AFTER_NAVIGATION_MESSAGE =
'This page was closed or refreshed. Upload will continue automatically when this browser returns.'
'This page was closed or refreshed. Upload will continue automatically when you return here in the same browser on this device.'
type TransferContextValue = {
transfers: LocalTransferRecord[]
@@ -160,6 +160,27 @@ describe('UploadStudio', () => {
draftState.sources = []
})
it('shows browser-first positioning with agent workflow support copy', async () => {
renderStudio()
expect(await screen.findByText('Encrypted file transfer.')).toBeInTheDocument()
expect(
screen.getByText(
/agent-driven terminal workflows, keeping plaintext file names, contents, and keys off the server\./,
),
).toBeInTheDocument()
expect(screen.getByText(/Use Xdrop in the browser for normal sharing, or/i)).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'through an agent' })).toHaveAttribute(
'href',
'https://github.com/xixu-me/xdrop?tab=readme-ov-file#use-via-agents',
)
expect(
screen.getByText(
/Use the web app to drop files or a folder\. Names, paths, and file contents are encrypted in this browser before upload\./,
),
).toBeInTheDocument()
})
it('blocks selections that exceed the transfer size limit', async () => {
const { container } = renderStudio()
+18 -4
View File
@@ -246,6 +246,19 @@ export function UploadStudio() {
<p className="eyebrow">End-to-end encryption</p>
<h1 className="page-hero-title">Encrypted file transfer.</h1>
<p className="lead">{PROJECT_ONE_LINER}</p>
<p className="muted">
Use Xdrop in the browser for normal sharing, or{' '}
<a
className="inline-link"
href="https://github.com/xixu-me/xdrop?tab=readme-ov-file#use-via-agents"
rel="noreferrer"
target="_blank"
>
through an agent
</a>{' '}
when you need to move files out of a cloud server, remote container, or automated
terminal workflow.
</p>
</div>
</div>
<div
@@ -265,8 +278,8 @@ export function UploadStudio() {
>
<div className="dropzone-shell">
<p className="muted dropzone-copy">
Drop files or a folder. Names, paths, and file contents are encrypted in this browser
before upload.
Use the web app to drop files or a folder. Names, paths, and file contents are
encrypted in this browser before upload.
</p>
{uploadError ? <p className="warning dropzone-copy">{uploadError}</p> : null}
<div className="button-row">
@@ -401,8 +414,9 @@ export function UploadStudio() {
</div>
</label>
<p className="warning">
Uploads continue automatically when this browser returns after a refresh or reopen.
Privacy mode keeps less sensitive state on this device, with fewer local controls.
Uploads continue automatically when you return here in the same browser on this device
after a refresh or reopen. Privacy mode keeps less sensitive state on this device, with
fewer local controls.
</p>
</Card>
+1 -1
View File
@@ -858,7 +858,7 @@ h3 {
0 18px 30px rgba(2, 5, 2, 0.18);
}
.dropzone > .warning {
.dropzone-shell > .warning {
margin-top: 0;
}
+19 -2
View File
@@ -4,9 +4,26 @@
export const SITE_NAME = 'Xdrop'
export const PROJECT_ONE_LINER =
'Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server.'
export const DEFAULT_SEO_TITLE = 'Open Source Encrypted File Transfer in the Browser | Xdrop'
'Xdrop is an open source encrypted file transfer app for browsers and agent-driven terminal workflows, keeping plaintext file names, contents, and keys off the server.'
export const SHORT_POSITIONING_SUMMARY =
'Browser-first encrypted file transfer, with agent-ready terminal workflows.'
export const TERMINAL_SUPPORT_BLURB =
'Use Xdrop in the browser for normal sharing, or through an agent when you need to move files out of a cloud server, remote container, or automated terminal workflow.'
export const DEFAULT_SEO_TITLE =
'Open Source Encrypted File Transfer for Browsers and Agents | Xdrop'
export const DEFAULT_SEO_DESCRIPTION = PROJECT_ONE_LINER
export const HISTORY_PAGE_TITLE = 'Manage Transfers on This Device | Xdrop'
export const HISTORY_PAGE_DESCRIPTION =
'Manage encrypted transfers stored in this browser on this device. There is no account or cross-device history.'
export const SHARE_PAGE_TITLE = 'Share the Full Link | Xdrop'
export const SHARE_PAGE_DESCRIPTION =
'Review upload status and copy the full share link for a transfer staged in this browser on this device.'
export const RECEIVE_PAGE_TITLE = 'Download and Decrypt in the Browser | Xdrop'
export const RECEIVE_PAGE_DESCRIPTION =
'Download files from this transfer and decrypt them in the browser. The decryption key stays in the share link fragment.'
export const NOT_FOUND_PAGE_TITLE = 'Page Not Found | Xdrop'
export const NOT_FOUND_PAGE_DESCRIPTION =
'The address does not map to a page in Xdrop. If this came from a shared transfer, ask for the full URL, including the #k=... decryption fragment.'
export const DEFAULT_OG_IMAGE_PATH = '/brand-lockup-horizontal.png'
export const DEFAULT_OG_IMAGE_ALT = 'Xdrop horizontal brand lockup'
export const DEFAULT_LOGO_PATH = '/brand-symbol-512.png'
@@ -27,7 +27,7 @@ describe('usePageMetadata', () => {
render(
<MemoryRouter initialEntries={['/']}>
<MetadataProbe
title="Open Source Encrypted File Transfer in the Browser | Xdrop"
title="Open Source Encrypted File Transfer for Browsers and Agents | Xdrop"
description="SEO test description"
structuredData={{
'@context': 'https://schema.org',
@@ -38,11 +38,13 @@ describe('usePageMetadata', () => {
</MemoryRouter>,
)
expect(document.title).toBe('Open Source Encrypted File Transfer in the Browser | Xdrop')
expect(document.title).toBe(
'Open Source Encrypted File Transfer for Browsers and Agents | Xdrop',
)
expect(readMetaByName('description')).toBe('SEO test description')
expect(readMetaByName('robots')).toContain('index, follow')
expect(readMetaByProperty('og:title')).toBe(
'Open Source Encrypted File Transfer in the Browser | Xdrop',
'Open Source Encrypted File Transfer for Browsers and Agents | Xdrop',
)
expect(readMetaByProperty('og:url')).toBe(new URL('/', window.location.origin).toString())
expect(readCanonical()).toBe(new URL('/', window.location.origin).toString())
@@ -89,7 +91,7 @@ describe('usePageMetadata', () => {
expect(document.head.querySelectorAll('meta[name="description"]')).toHaveLength(1)
expect(document.head.querySelectorAll('link[rel="canonical"]')).toHaveLength(1)
expect(readMetaByName('description')).toContain(
'Xdrop is an open source file transfer app that encrypts files in your browser',
'Xdrop is an open source encrypted file transfer app for browsers and agent-driven terminal workflows',
)
expect(readMetaByName('robots')).toContain('index, follow')
expect(readMetaByProperty('og:type')).toBe('website')