Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
131f1e9
feat(connections): let a connection be tried before it has to be save…
deveshk0 Sep 8, 2026
8bc5168
fix(connections): close the three ways the save offer could mislead (…
deveshk0 Sep 8, 2026
64b48e4
fix(connections): give back a connection nobody is waiting for (#369 …
deveshk0 Sep 8, 2026
4030f18
test(indexes): wait for the schema fields, not just for the call (#36…
deveshk0 Sep 8, 2026
9f1edcc
fix(connections): stop an unsaved connection leaking into places it c…
deveshk0 Sep 8, 2026
da23591
fix(connections): keep an unsaved tab out of the mirrored split, and …
deveshk0 Sep 8, 2026
204169b
Revert "split_pane" half of da23591 — it needs a product decision, no…
deveshk0 Sep 8, 2026
41887aa
fix(connections): let the offer describe the connection that was made…
deveshk0 Sep 8, 2026
9f31a2f
fix(sidebar): refuse a shortcut that could not survive a restart (#36…
deveshk0 Sep 8, 2026
7c49485
fix(connections): show the URI that was connected, and decide shortcu…
deveshk0 Sep 8, 2026
c866ab2
fix(connections): stop an unsaved session answering to a saved profil…
deveshk0 Sep 8, 2026
c5bc1fb
fix(connections): keep an unsaved session out of a saved profile's qu…
deveshk0 Sep 8, 2026
a377e30
fix(connections): take Math.random out of an id that now keys storage…
deveshk0 Sep 8, 2026
b6d91fd
fix(connections): finish isolating an unsaved session's query, chat a…
deveshk0 Sep 8, 2026
6b48634
fix(connections): let only the current attempt release the Connect bu…
deveshk0 Sep 8, 2026
7385899
fix(connections): isolate history writes and the shell assistant, and…
deveshk0 Sep 8, 2026
1a08ba1
fix(connections): read ephemeral from the profile, and release a sess…
deveshk0 Sep 8, 2026
6d29038
fix(connections): make the post-await ownership checks actually able …
deveshk0 Sep 8, 2026
aa66bd5
fix(connections): retarget a trial connection's chats under its own i…
deveshk0 Sep 8, 2026
c942548
fix(connections): stop the name suggestion renaming a profile called …
deveshk0 Sep 9, 2026
29c33ff
fix(connections): advance the attempt generation when the editor clos…
deveshk0 Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
466 changes: 435 additions & 31 deletions src/components/ConnectionManager.tsx

Large diffs are not rendered by default.

648 changes: 647 additions & 1 deletion src/components/__tests__/ConnectionManager.test.tsx

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions src/components/__tests__/IndexModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,17 @@ describe('IndexModal Component', () => {
}));
});

const fieldSelect = screen.getByTestId('index-key-field-0') as HTMLSelectElement;
const optionValues = Array.from(fieldSelect.options).map((o) => o.value);
expect(optionValues).toContain('email');
// Waited on the options themselves, not just on the call being made. The
// call having been issued says nothing about its promise having resolved
// and the state having been committed, so reading the list straight after
// was a race the test lost whenever CI was loaded enough.
const optionsNow = () => {
const select = screen.getByTestId('index-key-field-0') as HTMLSelectElement;
return Array.from(select.options).map((o) => o.value);
};
await waitFor(() => expect(optionsNow()).toContain('email'));

const optionValues = optionsNow();
expect(optionValues).toContain('profile.city');
expect(optionValues[0]).toBe('_id');
});
Expand Down
5 changes: 5 additions & 0 deletions src/locales/de/connections.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
"slate": "Schiefer",
"violet": "Violett"
},
"connectNow": {
"connected": "Verbunden",
"saveOffer": "Speichere diese Verbindung, damit du sie später wieder öffnen kannst.",
"dontSave": "Nicht speichern"
},
"connectionMode": {
"confirmDestructive": {
"description": "Destruktive Vorgänge müssen durch Eintippen bestätigt werden",
Expand Down
5 changes: 5 additions & 0 deletions src/locales/en/connections.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@
"label": "Read-only"
}
},
"connectNow": {
"connected": "Connected",
"dontSave": "Don't save",
"saveOffer": "Save this connection so you can open it again later."
},
"dialogs": {
"deleteProfile": {
"message": "Are you sure you want to delete this connection profile?",
Expand Down
5 changes: 5 additions & 0 deletions src/locales/zh-Hans/connections.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
"slate": "石板灰",
"violet": "紫罗兰色"
},
"connectNow": {
"connected": "已连接",
"saveOffer": "保存此连接,以便以后再次打开。",
"dontSave": "不保存"
},
"connectionMode": {
"confirmDestructive": {
"description": "破坏性操作需要输入名称确认",
Expand Down
40 changes: 40 additions & 0 deletions src/workspace/__tests__/persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
toDisconnectedSnapshot,
rebindConnection,
toProfileSpaceId,
isEphemeralProfileId,
toLiveSpaceId,
materializeArrivingTab,
carriedDocumentEdit,
Expand Down Expand Up @@ -668,3 +669,42 @@ describe('actionToOp', () => {
expect(actionToOp(action)).toEqual({ ...expected, window_id: 'main' });
});
});

describe('tabs on a connection that was never saved (#369 review)', () => {
const tab: PersistableTab = {
id: 'live-conn-1:users',
connectionId: 'live-conn-1',
type: 'collection',
db: 'app',
collection: 'users',
};

it('are not persisted, because there is no profile to reconnect them to', () => {
// Persisted, the id becomes `profile:ephemeral:...`, and on restart
// handleReconnectProfile resolves that against load_connection_profiles —
// which can never list it. The tab would be stuck on "profile missing".
const ephemeral = {
id: 'live-conn-1',
profileId: 'ephemeral:11111111-2222-3333-4444-555555555555',
name: 'trial',
};
expect(toPersistedTab(tab, ephemeral, undefined)).toBeNull();
});

it('are persisted as normal once the connection has been saved', () => {
const saved = { id: 'live-conn-1', profileId: 'profile-1', name: 'Prod' };
const persisted = toPersistedTab(tab, saved, undefined);
expect(persisted).not.toBeNull();
expect(persisted!.profileId).toBe('profile-1');
expect(persisted!.id).toBe('profile:profile-1:users');
});

it('recognises an ephemeral id only by its own prefix', () => {
expect(isEphemeralProfileId('ephemeral:abc')).toBe(true);
expect(isEphemeralProfileId('profile-1')).toBe(false);
expect(isEphemeralProfileId('')).toBe(false);
expect(isEphemeralProfileId(undefined)).toBe(false);
// Not a prefix match anywhere else in the string.
expect(isEphemeralProfileId('not-ephemeral:abc')).toBe(false);
});
});
16 changes: 16 additions & 0 deletions src/workspace/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ export interface RestoredTab {
// `toPersistedTab` returning null for it is what App.tsx's `dispatchWorkspace`
// reads to populate `unmirroredTabIdsRef`, so no separate change is needed.
const NON_PERSISTED_TYPES = new Set<QueryTabType>(['export', 'import', 'generate']);
/**
* Marks a connection the user opened but never saved (#364). Connection ids
* from `connect_db` are session-scoped, so an unsaved connection still needs
* *some* stable identity while it is open — the app dedupes on it — but that
* identity is not a profile and will never be one.
*/
export const EPHEMERAL_PROFILE_PREFIX = 'ephemeral:';
export const isEphemeralProfileId = (profileId: string | undefined | null): boolean =>
!!profileId && profileId.startsWith(EPHEMERAL_PROFILE_PREFIX);
// These tab kinds carry no connection at all; pass their id through as-is.
const CONNECTIONLESS_TYPES = new Set<QueryTabType>(['settings', 'quickstart', 'tasks', 'activity']);

Expand Down Expand Up @@ -201,6 +210,13 @@ export function toPersistedTab(
): PersistedTab | null {
if (NON_PERSISTED_TYPES.has(tab.type)) return null;

// A connection that was never saved has no profile to come back to. Its tabs
// would persist as `profile:ephemeral:<uuid>`, and on restart
// `handleReconnectProfile` resolves that against `load_connection_profiles`,
// which can never list it — so the tab would sit on "profile missing"
// forever. Dropped at save time instead, like an export tab (#369 review).
if (conn && isEphemeralProfileId(conn.profileId)) return null;
Comment thread
deveshk0 marked this conversation as resolved.

if (CONNECTIONLESS_TYPES.has(tab.type)) {
return {
id: tab.id,
Expand Down