feat(FN-2519): add remote tunnel manager for settings sync

- Add remote-access contracts, provider adapters, and a tunnel process manager with lifecycle handling
- Wire tunnel manager into ProjectEngine startup/shutdown flow and export new remote-access modules
- Update settings modal UX for remote auth URLs, including wrapping and related UI test coverage
- Document tunnel manager behavior and remote settings sync details in architecture, CLI, and settings docs
This commit is contained in:
Fusion
2026-04-26 02:20:28 -07:00
committed by gsxdsm
parent 7527a7ddaf
commit cb1da0c659
16 changed files with 1234 additions and 28 deletions

View File

@@ -272,10 +272,26 @@
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--text-muted);
max-height: 220px;
max-height: calc(var(--space-2xl) * 7);
overflow: auto;
font-family: var(--font-mono);
font-size: 12px;
font-size: calc(var(--space-sm) + var(--space-xs));
}
.settings-url-output {
display: block;
margin-top: var(--space-xs);
padding: var(--space-sm);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--text-muted);
font-family: var(--font-mono);
font-size: calc(var(--space-sm) + var(--space-xs));
overflow-wrap: anywhere;
word-break: break-word;
overflow-x: auto;
max-width: 100%;
}
.settings-qr-preview {
@@ -287,12 +303,13 @@
.settings-qr-preview-label {
margin: 0;
font-size: 12px;
font-size: calc(var(--space-sm) + var(--space-xs));
color: var(--text-muted);
}
.settings-qr-preview-image-wrap {
width: fit-content;
max-width: 100%;
padding: var(--space-sm);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
@@ -301,9 +318,9 @@
.settings-qr-preview-image {
display: block;
width: calc(var(--space-2xl) * 6);
height: calc(var(--space-2xl) * 6);
max-width: 100%;
width: min(calc(var(--space-2xl) * 6), 100%);
height: auto;
aspect-ratio: 1 / 1;
}
.settings-overlap-ignore-list {

View File

@@ -3531,7 +3531,12 @@ export function SettingsModal({
<div className="form-group">
<label>Tunnel lifecycle</label>
<small>State: <strong>{tunnelState}</strong>{remoteStatus?.provider ? ` · Provider: ${remoteStatus.provider}` : ""}</small>
{remoteStatus?.url && <small>URL: <code>{remoteStatus.url}</code></small>}
{remoteStatus?.url && (
<small>
URL:
<code className="settings-url-output">{remoteStatus.url}</code>
</small>
)}
{remoteStatus?.lastError && <small className="field-error">{remoteStatus.lastError}</small>}
<div className="settings-button-row">
<button
@@ -3635,7 +3640,8 @@ export function SettingsModal({
)}
{remoteUrlPreview?.url && (
<small>
Authenticated URL: <code>{remoteUrlPreview.url}</code>
Authenticated URL:
<code className="settings-url-output">{remoteUrlPreview.url}</code>
</small>
)}
{remoteQrSvg && (

View File

@@ -702,7 +702,7 @@ describe("SettingsModal", () => {
expect(mockFetchSettings).toHaveBeenCalled();
});
expect(screen.getByText("Memory")).toBeDefined();
expect(await screen.findByText("Memory")).toBeDefined();
});
it("shows the memory toggle with default enabled", async () => {
@@ -713,7 +713,7 @@ describe("SettingsModal", () => {
});
// Click the Memory section in the sidebar
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
const checkbox = screen.getByRole("checkbox", { name: /enable memory tools/i });
expect(checkbox).toBeDefined();
@@ -734,7 +734,7 @@ describe("SettingsModal", () => {
});
// Click the Memory section in the sidebar
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
const checkbox = screen.getByRole("checkbox", { name: /enable memory tools/i });
expect(checkbox).toBeDefined();
@@ -749,7 +749,7 @@ describe("SettingsModal", () => {
});
// Click the Memory section in the sidebar
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
const checkbox = screen.getByRole("checkbox", { name: /enable memory tools/i });
expect(checkbox).toBeChecked();
@@ -798,7 +798,7 @@ describe("SettingsModal", () => {
await waitFor(() => {
expect(mockFetchSettings).toHaveBeenCalled();
});
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
expect(screen.getByText("Checking memory write access...")).toBeInTheDocument();
expect(screen.queryByText(/qmd is not installed\. Search will use local files\./i)).not.toBeInTheDocument();
@@ -839,7 +839,7 @@ describe("SettingsModal", () => {
await waitFor(() => {
expect(mockFetchSettings).toHaveBeenCalled();
});
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
expect(screen.getByText(/Memory is configured with a read-only backend\./i)).toBeInTheDocument();
});
@@ -880,7 +880,7 @@ describe("SettingsModal", () => {
await waitFor(() => {
expect(mockFetchSettings).toHaveBeenCalled();
});
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
await userEvent.click(await screen.findByRole("button", { name: "Install qmd" }));
@@ -900,7 +900,7 @@ describe("SettingsModal", () => {
expect(mockFetchMemoryFiles).not.toHaveBeenCalled();
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
await waitFor(() => {
expect(mockFetchMemoryFiles).toHaveBeenCalledWith(undefined);
@@ -925,7 +925,7 @@ describe("SettingsModal", () => {
expect(mockFetchSettings).toHaveBeenCalled();
});
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
expect(screen.getByText("Loading memory…")).toBeDefined();
@@ -944,7 +944,7 @@ describe("SettingsModal", () => {
expect(mockFetchSettings).toHaveBeenCalled();
});
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
await waitFor(() => {
expect(mockFetchMemoryFile).toHaveBeenCalledWith(".fusion/memory/DREAMS.md", undefined);
@@ -977,7 +977,7 @@ describe("SettingsModal", () => {
expect(mockFetchSettings).toHaveBeenCalled();
});
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
const compactButton = await screen.findByRole("button", { name: "Compact Selected File" });
await userEvent.click(compactButton);
@@ -999,7 +999,7 @@ describe("SettingsModal", () => {
expect(mockFetchSettings).toHaveBeenCalled();
});
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
const editor = await screen.findByLabelText("Editor for .fusion/memory/DREAMS.md") as HTMLTextAreaElement;
expect(editor.value).toBe("");
@@ -1019,7 +1019,7 @@ describe("SettingsModal", () => {
expect(mockFetchSettings).toHaveBeenCalled();
});
await userEvent.click(screen.getByText("Memory"));
await userEvent.click(await screen.findByText("Memory"));
const select = await screen.findByLabelText("Memory File");
await userEvent.selectOptions(select, ".fusion/memory/DREAMS.md");