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:
@@ -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 {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user