Skip to content

Commit d20a8ce

Browse files
juliusmarmingecodex
andcommitted
Move mobile and shared runtime cleanups below relay
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 023ca4c commit d20a8ce

11 files changed

Lines changed: 218 additions & 71 deletions

File tree

apps/mobile/src/features/threads/use-project-actions.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import {
1313
type RuntimeMode,
1414
} from "@t3tools/contracts";
1515
import { buildTemporaryWorktreeBranchName, sanitizeFeatureBranchName } from "@t3tools/shared/git";
16+
import { uuidv4 } from "../../lib/uuid";
1617

1718
import type { DraftComposerImageAttachment } from "../../lib/composerImages";
18-
import { uuidv4 } from "../../lib/uuid";
19+
import { makeTurnCommandMetadata } from "../../lib/commandMetadata";
1920
import { getEnvironmentClient } from "../../state/environment-session-registry";
2021
import { environmentRuntimeManager } from "../../state/use-environment-runtime";
2122
import { vcsRefManager } from "../../state/use-vcs-refs";
@@ -93,8 +94,9 @@ export function useProjectActions() {
9394
return null;
9495
}
9596

96-
const threadId = ThreadId.make(uuidv4());
97-
const createdAt = new Date().toISOString();
97+
const metadata = makeTurnCommandMetadata();
98+
const threadId = ThreadId.make(metadata.threadId);
99+
const createdAt = metadata.createdAt;
98100
const initialMessageText = input.initialMessageText.trim();
99101
const nextTitle = deriveThreadTitleFromPrompt(input.initialMessageText);
100102

@@ -109,10 +111,10 @@ export function useProjectActions() {
109111

110112
await client.orchestration.dispatchCommand({
111113
type: "thread.turn.start",
112-
commandId: CommandId.make(uuidv4()),
114+
commandId: CommandId.make(metadata.commandId),
113115
threadId,
114116
message: {
115-
messageId: MessageId.make(uuidv4()),
117+
messageId: MessageId.make(metadata.messageId),
116118
role: "user",
117119
text: initialMessageText,
118120
attachments: input.initialAttachments,
@@ -143,7 +145,7 @@ export function useProjectActions() {
143145
}
144146
: {}),
145147
},
146-
createdAt: new Date().toISOString(),
148+
createdAt,
147149
});
148150

149151
await refreshRemoteData([input.project.environmentId]);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { makeQueuedMessageMetadata, makeTurnCommandMetadata } from "./commandMetadata";
4+
5+
vi.mock("expo-crypto", () => ({
6+
randomUUID: () => crypto.randomUUID(),
7+
}));
8+
9+
describe("mobile command metadata", () => {
10+
it("creates ids and timestamps for thread starts", () => {
11+
const metadata = makeTurnCommandMetadata();
12+
13+
expect(metadata.commandId).toMatch(
14+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
15+
);
16+
expect(metadata.messageId).toMatch(
17+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
18+
);
19+
expect(metadata.threadId).toMatch(
20+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
21+
);
22+
expect(metadata.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
23+
});
24+
25+
it("creates ids and timestamps for queued messages", () => {
26+
const metadata = makeQueuedMessageMetadata();
27+
28+
expect(metadata.commandId).toMatch(
29+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
30+
);
31+
expect(metadata.messageId).toMatch(
32+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
33+
);
34+
expect(metadata.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
35+
});
36+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { uuidv4 } from "./uuid";
2+
3+
export interface TurnCommandMetadata {
4+
readonly commandId: string;
5+
readonly messageId: string;
6+
readonly threadId: string;
7+
readonly createdAt: string;
8+
}
9+
10+
export function makeTurnCommandMetadata(): TurnCommandMetadata {
11+
return {
12+
commandId: uuidv4(),
13+
messageId: uuidv4(),
14+
threadId: uuidv4(),
15+
createdAt: new Date().toISOString(),
16+
};
17+
}
18+
19+
export function makeQueuedMessageMetadata(): Omit<TurnCommandMetadata, "threadId"> {
20+
return {
21+
commandId: uuidv4(),
22+
messageId: uuidv4(),
23+
createdAt: new Date().toISOString(),
24+
};
25+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { redactPairingCredential } from "./connection";
4+
5+
vi.mock("./runtime", () => ({
6+
mobileRemoteHttpRuntime: {
7+
runPromise: vi.fn(),
8+
},
9+
}));
10+
11+
describe("mobile remote connection records", () => {
12+
it("removes one-time bootstrap credentials before persisting pairing URLs", () => {
13+
expect(redactPairingCredential("https://desktop.example/#token=bootstrap-token")).toBe(
14+
"https://desktop.example/",
15+
);
16+
expect(redactPairingCredential("https://desktop.example/?token=bootstrap-token")).toBe(
17+
"https://desktop.example/",
18+
);
19+
});
20+
21+
it("removes hosted pairing credentials while keeping the advertised host", () => {
22+
expect(
23+
redactPairingCredential(
24+
"https://app.t3.codes/pair?host=https%3A%2F%2Fdesktop.example&token=bootstrap-token&label=Desktop",
25+
),
26+
).toBe("https://app.t3.codes/pair?host=https%3A%2F%2Fdesktop.example&label=Desktop");
27+
});
28+
});

apps/mobile/src/lib/connection.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
bootstrapRemoteBearerSession,
44
fetchRemoteEnvironmentDescriptor,
55
} from "@t3tools/client-runtime";
6-
import { resolveRemotePairingTarget } from "@t3tools/shared/remote";
6+
import { resolveRemotePairingTarget, stripPairingTokenFromUrl } from "@t3tools/shared/remote";
77
import { mobileRemoteHttpRuntime } from "./runtime";
88

99
export interface RemoteConnectionInput {
@@ -27,6 +27,15 @@ export type RemoteClientConnectionState =
2727
| "reconnecting"
2828
| "disconnected";
2929

30+
export function redactPairingCredential(pairingUrl: string): string {
31+
const trimmed = pairingUrl.trim();
32+
try {
33+
return stripPairingTokenFromUrl(new URL(trimmed)).toString();
34+
} catch {
35+
return trimmed;
36+
}
37+
}
38+
3039
export async function bootstrapRemoteConnection(
3140
input: RemoteConnectionInput,
3241
): Promise<SavedRemoteConnection> {
@@ -50,7 +59,7 @@ export async function bootstrapRemoteConnection(
5059
return {
5160
environmentId: descriptor.environmentId,
5261
environmentLabel: descriptor.label,
53-
pairingUrl: input.pairingUrl.trim(),
62+
pairingUrl: redactPairingCredential(input.pairingUrl),
5463
displayUrl: target.httpBaseUrl,
5564
httpBaseUrl: target.httpBaseUrl,
5665
wsBaseUrl: target.wsBaseUrl,

apps/mobile/src/state/use-remote-catalog.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ const projectsSortOrder = Order.mapInput(
3131

3232
const threadsSortOrder = Order.mapInput(
3333
Order.Struct({
34-
activityAt: Order.flip(Order.Number),
34+
activityAt: Order.flip(Order.String),
3535
environmentId: Order.String,
3636
}),
3737
(thread: EnvironmentScopedThreadShell) => ({
38-
activityAt: new Date(thread.updatedAt ?? thread.createdAt).getTime(),
38+
activityAt: thread.updatedAt ?? thread.createdAt,
3939
environmentId: thread.environmentId,
4040
}),
4141
);

apps/mobile/src/state/use-thread-composer-state.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CommandId, MessageId, type EnvironmentId, type ThreadId } from "@t3tool
66
import { deriveActiveWorkStartedAt } from "@t3tools/shared/orchestrationTiming";
77
import { Atom } from "effect/unstable/reactivity";
88

9+
import { makeQueuedMessageMetadata } from "../lib/commandMetadata";
910
import {
1011
convertPastedImagesToAttachments,
1112
pasteComposerClipboard,
@@ -14,7 +15,6 @@ import {
1415
import type { DraftComposerImageAttachment } from "../lib/composerImages";
1516
import { scopedThreadKey } from "../lib/scopedEntities";
1617
import { buildThreadFeed, type QueuedThreadMessage } from "../lib/threadActivity";
17-
import { uuidv4 } from "../lib/uuid";
1818
import { appAtomRegistry } from "../state/atom-registry";
1919
import { getEnvironmentClient } from "./environment-session-registry";
2020
import type { ConnectedEnvironmentSummary } from "../state/remote-runtime-types";
@@ -356,15 +356,15 @@ export function useThreadComposerState() {
356356
return;
357357
}
358358

359-
const createdAt = new Date().toISOString();
359+
const metadata = makeQueuedMessageMetadata();
360360
enqueueQueuedMessage({
361361
environmentId: selectedThreadShell.environmentId,
362362
threadId: selectedThreadShell.id,
363-
messageId: MessageId.make(uuidv4()),
364-
commandId: CommandId.make(uuidv4()),
363+
messageId: MessageId.make(metadata.messageId),
364+
commandId: CommandId.make(metadata.commandId),
365365
text,
366366
attachments,
367-
createdAt,
367+
createdAt: metadata.createdAt,
368368
});
369369
clearDraft(threadKey);
370370
}, [draftAttachmentsByThreadKey, draftMessageByThreadKey, selectedThreadShell]);

0 commit comments

Comments
 (0)