Skip to content

Commit 3d99e46

Browse files
feat: default task Browser iframe to Tailscale IP when available
Detect the host's Tailscale IPv4 on demand via `tailscale status --json` and use it as the default URL for the per-task Browser tab iframe (`http://<ip>:3000`), falling back to `http://localhost:3000` when Tailscale isn't running. Surfaced through the existing config endpoint as a read-only computed key (`tailscale_ip`), like `home_dir` — no new persisted setting, no fnox plumbing, and no dependency on the user having run `fulcrum expose`. The dev server is reachable from any tailnet client out of the box, even when MagicDNS isn't configured. chore: bump version to 5.8.0
1 parent 99eae64 commit 3d99e46

12 files changed

Lines changed: 63 additions & 25 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"name": "fulcrum",
99
"source": "./plugins/fulcrum",
1010
"description": "Task orchestration for Claude Code",
11-
"version": "5.7.1"
11+
"version": "5.8.0"
1212
}
1313
]
1414
}

cli/src/mcp/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export async function runMcpServer(urlOverride?: string, portOverride?: string)
1212

1313
const server = new McpServer({
1414
name: 'fulcrum',
15-
version: '5.7.1',
15+
version: '5.8.0',
1616
})
1717

1818
registerTools(server, client)

desktop/neutralino.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
33
"applicationId": "io.fulcrum.desktop",
4-
"version": "5.7.1",
4+
"version": "5.8.0",
55
"defaultMode": "window",
66
"port": 0,
77
"documentRoot": "/resources/",
@@ -26,7 +26,7 @@
2626
],
2727
"globalVariables": {
2828
"APP_NAME": "Fulcrum",
29-
"APP_VERSION": "5.7.1"
29+
"APP_VERSION": "5.8.0"
3030
},
3131
"modes": {
3232
"window": {

frontend/hooks/use-config.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ export function usePort() {
3636
}
3737

3838
// Tailscale hostname for this Fulcrum host. Populated by `fulcrum expose`.
39-
// When non-null and the UI is served from a non-localhost host, the task
40-
// browser preview rewrites `localhost:<port>` URLs to this hostname.
4139
export function useTailscaleHostname() {
4240
const query = useConfig(CONFIG_KEYS.TAILSCALE_HOSTNAME)
4341

@@ -48,6 +46,19 @@ export function useTailscaleHostname() {
4846
}
4947
}
5048

49+
// Tailscale IPv4 address for this Fulcrum host. Populated by `fulcrum expose`.
50+
// Preferred over the hostname for browser preview defaults since IPs always
51+
// resolve from any tailnet client.
52+
export function useTailscaleIp() {
53+
const query = useConfig(CONFIG_KEYS.TAILSCALE_IP)
54+
55+
return {
56+
...query,
57+
data: (query.data?.value as string | null) ?? null,
58+
isDefault: query.data?.isDefault ?? true,
59+
}
60+
}
61+
5162
// Public domain serving this Fulcrum instance through Cloudflare Tunnel.
5263
export function usePublicDomain() {
5364
const query = useConfig(CONFIG_KEYS.PUBLIC_DOMAIN)

frontend/hooks/use-task-view-state.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useQueryClient, useMutation } from '@tanstack/react-query'
22
import { useCallback, useEffect, useMemo, useRef } from 'react'
33
import { useTask } from './use-tasks'
4+
import { useTailscaleIp } from './use-config'
45
import type { Task, ViewState, DiffOptions, FilesViewState } from '@/types'
56

67
interface PendingUpdates {
@@ -10,11 +11,11 @@ interface PendingUpdates {
1011
filesViewState?: Partial<FilesViewState>
1112
}
1213

13-
const getDefaultBrowserUrl = () => 'http://localhost:3000'
14+
const LOCALHOST_BROWSER_URL = 'http://localhost:3000'
1415

1516
const DEFAULT_VIEW_STATE: ViewState = {
1617
activeTab: 'diff',
17-
browserUrl: getDefaultBrowserUrl(),
18+
browserUrl: LOCALHOST_BROWSER_URL,
1819
diffOptions: {
1920
wrap: true,
2021
ignoreWhitespace: true,
@@ -35,15 +36,22 @@ export function useTaskViewState(taskId: string) {
3536
const latestViewStateRef = useRef<ViewState>(DEFAULT_VIEW_STATE)
3637

3738
const { data: task } = useTask(taskId)
39+
const { data: tailscaleIp } = useTailscaleIp()
40+
41+
const defaultBrowserUrl = tailscaleIp
42+
? `http://${tailscaleIp}:3000`
43+
: LOCALHOST_BROWSER_URL
3844

3945
// Parse viewState from task, merge with defaults
4046
const viewState: ViewState = useMemo(() => {
4147
const stored = task?.viewState
42-
if (!stored) return DEFAULT_VIEW_STATE
48+
if (!stored) {
49+
return { ...DEFAULT_VIEW_STATE, browserUrl: defaultBrowserUrl }
50+
}
4351

4452
return {
4553
activeTab: stored.activeTab ?? DEFAULT_VIEW_STATE.activeTab,
46-
browserUrl: stored.browserUrl ?? DEFAULT_VIEW_STATE.browserUrl,
54+
browserUrl: stored.browserUrl ?? defaultBrowserUrl,
4755
diffOptions: {
4856
...DEFAULT_VIEW_STATE.diffOptions,
4957
...stored.diffOptions,
@@ -53,7 +61,7 @@ export function useTaskViewState(taskId: string) {
5361
...stored.filesViewState,
5462
},
5563
}
56-
}, [task?.viewState])
64+
}, [task?.viewState, defaultBrowserUrl])
5765

5866
useEffect(() => {
5967
latestViewStateRef.current = viewState

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@knowsuchagency/fulcrum",
33
"private": true,
4-
"version": "5.7.1",
4+
"version": "5.8.0",
55
"description": "Harness Attention. Orchestrate Agents. Ship.",
66
"license": "PolyForm-Perimeter-1.0.0",
77
"type": "module",

plugins/fulcrum/.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"name": "fulcrum",
99
"source": "./",
1010
"description": "Task orchestration for Claude Code",
11-
"version": "5.7.1",
11+
"version": "5.8.0",
1212
"skills": [
1313
"./skills/fulcrum"
1414
],

plugins/fulcrum/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "fulcrum",
33
"description": "Fulcrum task orchestration for Claude Code",
4-
"version": "5.7.1",
4+
"version": "5.8.0",
55
"author": {
66
"name": "Fulcrum"
77
},

server/routes/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '../lib/settings'
2121
import { spawn } from 'child_process'
2222
import { testNotificationChannel, sendNotification, type NotificationPayload } from '../services/notification-service'
23+
import { detectTailscaleIp } from './server-expose'
2324

2425
export { CONFIG_KEYS } from '../../shared/config-keys'
2526
import { CONFIG_KEYS } from '../../shared/config-keys'
@@ -259,6 +260,9 @@ app.get('/:key', (c) => {
259260
if (key === 'home_dir') {
260261
return c.json({ key, value: os.homedir(), isDefault: true })
261262
}
263+
if (key === 'tailscale_ip') {
264+
return c.json({ key, value: detectTailscaleIp(), isDefault: true })
265+
}
262266

263267
// Resolve key to nested path
264268
const path = resolveConfigKey(key)

server/routes/mcp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ mcpRoutes.all('/', async (c) => {
2020
// Create MCP server
2121
const server = new McpServer({
2222
name: 'fulcrum',
23-
version: '5.7.1',
23+
version: '5.8.0',
2424
})
2525

2626
// Client connects back to this server

0 commit comments

Comments
 (0)