Updates for permanent repository URLs (#106)

This commit is contained in:
Brian Hackett 2025-04-15 16:58:08 -07:00 committed by GitHub
parent cb417d8384
commit f5cd0fd9f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 49 additions and 113 deletions

View File

@ -59,27 +59,30 @@ export const Menu = () => {
.catch((error) => toast.error(error.message));
}, []);
const deleteItem = useCallback((event: React.UIEvent, item: ChatContents) => {
event.preventDefault();
const deleteItem = useCallback(
(event: React.UIEvent, item: ChatContents) => {
event.preventDefault();
// Optimistically remove the item from the list while we update the database.
setList(list.filter((chat) => chat.id !== item.id));
// Optimistically remove the item from the list while we update the database.
setList(list.filter((chat) => chat.id !== item.id));
database
.deleteChat(item.id)
.then(() => {
loadEntries();
database
.deleteChat(item.id)
.then(() => {
loadEntries();
if (chatStore.currentChat.get()?.id === item.id) {
// hard page navigation to clear the stores
window.location.pathname = '/';
}
})
.catch((error) => {
toast.error('Failed to delete conversation');
logger.error(error);
});
}, [list]);
if (chatStore.currentChat.get()?.id === item.id) {
// hard page navigation to clear the stores
window.location.pathname = '/';
}
})
.catch((error) => {
toast.error('Failed to delete conversation');
logger.error(error);
});
},
[list],
);
const closeDialog = () => {
setDialogContent(null);

View File

@ -27,7 +27,6 @@ export const Preview = memo(() => {
const [selectionPoint, setSelectionPoint] = useState<{ x: number; y: number } | null>(null);
const previewURL = useStore(workbenchStore.previewURL);
const previewError = useStore(workbenchStore.previewError);
// Toggle between responsive mode and device mode
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
@ -278,9 +277,7 @@ export const Preview = memo(() => {
/>
</>
) : (
<div className="flex w-full h-full justify-center items-center bg-white">
{previewError ? 'Failed to load preview' : 'Preview loading...'}
</div>
<div className="flex w-full h-full justify-center items-center bg-white">Preview loading...</div>
)}
{isDeviceModeOn && (

View File

@ -69,7 +69,7 @@ async function getAllChats(): Promise<ChatContents[]> {
}
const chats = data.map(databaseRowToChatContents);
return chats.filter(chat => !deletedChats.has(chat.id));
return chats.filter((chat) => !deletedChats.has(chat.id));
}
async function syncLocalChats(): Promise<void> {

View File

@ -1,86 +1,20 @@
// Support using the Nut API for the development server.
// Support managing state for the development server URL the preview is loading.
import { assert, ProtocolClient } from './ReplayProtocolClient';
import { workbenchStore } from '~/lib/stores/workbench';
import { recordingMessageHandlerScript } from './Recording';
class DevelopmentServerManager {
// Empty if this chat has been destroyed.
client: ProtocolClient | undefined;
// Resolves when the chat has started.
chatIdPromise: Promise<string>;
constructor() {
this.client = new ProtocolClient();
this.chatIdPromise = (async () => {
assert(this.client, 'Chat has been destroyed');
await this.client.initialize();
const { chatId } = (await this.client.sendCommand({ method: 'Nut.startChat', params: {} })) as { chatId: string };
console.log('DevelopmentServerChat', new Date().toISOString(), chatId);
return chatId;
})();
function getRepositoryURL(repositoryId: string | undefined) {
if (!repositoryId) {
return undefined;
}
destroy() {
this.client?.close();
this.client = undefined;
}
async setRepositoryContents(repositoryId: string): Promise<string | undefined> {
assert(this.client, 'Chat has been destroyed');
try {
const chatId = await this.chatIdPromise;
const { url } = (await this.client.sendCommand({
method: 'Nut.startDevelopmentServer',
params: {
chatId,
repositoryId,
injectedScript: recordingMessageHandlerScript,
},
})) as { url: string };
return url;
} catch (e) {
console.error('DevelopmentServerError', e);
return undefined;
}
}
return `https://${repositoryId}.http.replay.io`;
}
let gActiveDevelopmentServer: DevelopmentServerManager | undefined;
export async function updateDevelopmentServer(repositoryId: string | undefined) {
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryId);
const repositoryURL = getRepositoryURL(repositoryId);
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryURL);
workbenchStore.showWorkbench.set(repositoryId !== undefined);
workbenchStore.repositoryId.set(repositoryId);
workbenchStore.previewURL.set(undefined);
workbenchStore.previewError.set(false);
if (!repositoryId) {
return;
}
if (!gActiveDevelopmentServer) {
gActiveDevelopmentServer = new DevelopmentServerManager();
}
const url = await gActiveDevelopmentServer.setRepositoryContents(repositoryId);
if (workbenchStore.repositoryId.get() != repositoryId) {
return;
}
if (url) {
workbenchStore.previewURL.set(url);
} else {
workbenchStore.previewError.set(true);
}
workbenchStore.showWorkbench.set(repositoryURL !== undefined);
workbenchStore.repositoryId.set(repositoryURL);
workbenchStore.previewURL.set(repositoryURL);
}

View File

@ -1,4 +1,9 @@
// Manage state around recording Preview behavior for generating a Replay recording.
//
// When updating this file the backend's injected script should be updated to match,
// see injectedScript.ts.
//
// Note that to make this easier we don't use backticks ` in this file.
import { createInjectableFunction } from './injectable';
import { assert, stringToBase64, uint8ArrayToBase64 } from './ReplayProtocolClient';
@ -208,7 +213,7 @@ const addRecordingMessageHandler = createInjectableFunction(
};
}
}
throw new Error(`Unknown request type: ${request}`);
throw new Error('Unknown request type: ' + request);
}
window.addEventListener('message', async (event) => {
@ -234,10 +239,10 @@ const addRecordingMessageHandler = createInjectableFunction(
height: rect.height,
/*
* at times `event.clientX` and `event.clientY` can be slighly off in relation to the element's position
* at times event.clientX and event.clientY can be slighly off in relation to the element's position
* it's possible that this position might lie outside the element's bounds
* the difference likely comes from a subpixel rounding or hit target calculation in the browser
* it's possible that we should account for `event.width` and `event.height` here but clamping the values to the bounds of the element should be good enough
* it's possible that we should account for event.width and event.height here but clamping the values to the bounds of the element should be good enough
*/
x: clamp(event.clientX - rect.x, 0, rect.width),
y: clamp(event.clientY - rect.y, 0, rect.height),
@ -264,7 +269,7 @@ const addRecordingMessageHandler = createInjectableFunction(
while (current) {
// If element has an ID, use it as it's the most specific
if (current.id) {
path.unshift(`#${current.id}`);
path.unshift('#' + current.id);
break;
}
@ -279,7 +284,7 @@ const addRecordingMessageHandler = createInjectableFunction(
const index = siblings.indexOf(current) + 1;
if (siblings.filter((el) => el.tagName === current!.tagName).length > 1) {
selector += `:nth-child(${index})`;
selector += ':nth-child(' + index + ')';
}
}
@ -354,7 +359,7 @@ const addRecordingMessageHandler = createInjectableFunction(
);
function onInterceptedOperation(_name: string) {
//console.log(`InterceptedOperation ${name}`);
//console.log("InterceptedOperation " + name);
}
function interceptProperty(obj: object, prop: string, interceptor: (basevalue: any) => any) {
@ -365,7 +370,7 @@ const addRecordingMessageHandler = createInjectableFunction(
Object.defineProperty(obj, prop, {
...descriptor,
get() {
onInterceptedOperation(`Getter:${prop}`);
onInterceptedOperation('Getter:' + prop);
if (!interceptValue) {
const baseValue = (descriptor?.get as any).call(obj);
@ -526,7 +531,7 @@ const addRecordingMessageHandler = createInjectableFunction(
return new Proxy(obj, {
get(target, prop) {
onInterceptedOperation(`ProxyGetter:${name}.${String(prop)}`);
onInterceptedOperation('ProxyGetter:' + name + '.' + String(prop));
let value = target[prop];
@ -542,7 +547,7 @@ const addRecordingMessageHandler = createInjectableFunction(
},
set(target, prop, value) {
onInterceptedOperation(`ProxySetter:${name}.${String(prop)}`);
onInterceptedOperation('ProxySetter:' + name + '.' + String(prop));
target[prop] = value;
return true;
@ -552,7 +557,7 @@ const addRecordingMessageHandler = createInjectableFunction(
function createFunctionProxy(fn: any, name: string, handler?: (v: any, ...args: any[]) => any) {
return (...args: any[]) => {
onInterceptedOperation(`FunctionCall:${name}`);
onInterceptedOperation('FunctionCall:' + name);
const v = fn(...args);

View File

@ -1,4 +1,4 @@
import { pingTelemetry } from '../hooks/pingTelemetry';
import { pingTelemetry } from '~/lib/hooks/pingTelemetry';
import { createInjectableFunction } from './injectable';
const replayWsServer = 'wss://dispatch.replay.io';

View File

@ -7,9 +7,6 @@ export class WorkbenchStore {
// Any available preview URL for the current repository.
previewURL = atom<string | undefined>(undefined);
// Whether there was an error loading the preview.
previewError = atom<boolean>(false);
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
constructor() {