Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
8acf756
feat(teleport): add the import/export engine without native formats
cursoragent Aug 15, 2026
75a3793
feat(teleport): add the import picker and export button
cursoragent Aug 15, 2026
1700d19
feat(teleport): add Codex native session support
cursoragent Aug 15, 2026
dbc8452
feat(teleport): add OpenCode native session support
cursoragent Aug 15, 2026
2048109
feat(teleport): add Claude native session support
cursoragent Aug 15, 2026
38eac2f
feat(teleport): add Grok native session support
cursoragent Aug 15, 2026
5062f81
fix(opencode): settle the session when a turn is aborted
cursoragent Aug 15, 2026
9de48da
fix(opencode): unstick Working when Stop has no live session
cursoragent Aug 15, 2026
3620294
test(teleport): use generic fixture paths
cursoragent Aug 15, 2026
6a51d21
test(teleport): use generic fixture paths
cursoragent Aug 15, 2026
f86f532
test(teleport): use generic fixture paths
cursoragent Aug 15, 2026
959ef15
test(teleport): use generic fixture paths
cursoragent Aug 15, 2026
d24730c
test(teleport): use generic fixture paths
cursoragent Aug 15, 2026
3ac60c2
test(teleport): use generic fixture paths
cursoragent Aug 15, 2026
9594bd8
test(teleport): use generic OpenCode fixture copy
cursoragent Aug 15, 2026
10503d6
merge(teleport): stack UI on sanitized backend
cursoragent Aug 15, 2026
5cca3e4
merge(teleport): stack Codex on UI
cursoragent Aug 15, 2026
daccf14
merge(teleport): stack Claude on Codex
cursoragent Aug 15, 2026
0c490e2
merge(teleport): stack Grok on Claude
cursoragent Aug 15, 2026
87fd928
merge(teleport): stack OpenCode on Grok
cursoragent Aug 15, 2026
18ed3c0
Merge branch 'main' into cursor/teleport-opencode-1e23
baanish Aug 15, 2026
b3ac6a8
fix(web): match teleport controls to existing header primitives
cursoragent Aug 16, 2026
468bc93
fix(teleport): close lock, path, and projection holes from review
cursoragent Aug 16, 2026
77fbd25
refactor(teleport): collocate TeleportService in one module
cursoragent Aug 16, 2026
9e479d0
fix(web): hide teleport when the server lacks the capability
cursoragent Aug 16, 2026
15193f8
fix(teleport): discover Codex and Claude sessions across instance homes
cursoragent Aug 16, 2026
d075c83
fix(teleport): keep busy threads and OpenCode paths from corrupting s…
cursoragent Aug 16, 2026
3b38975
fix(teleport): distinguish lock probes and write to instance homes
cursoragent Aug 16, 2026
95fba97
fix(teleport): keep custom instances on the selected native session
cursoragent Aug 16, 2026
d76928b
test(web): drop duplicated teleport capability cases
cursoragent Aug 16, 2026
1cb4f0f
fix(teleport): match existing threads by provider instance
cursoragent Aug 16, 2026
ef27858
fix(teleport): write OpenCode sqlite sessions the CLI can resume
cursoragent Aug 16, 2026
cb5633b
fix(teleport): keep export from splitting T3 and the native CLI
cursoragent Aug 16, 2026
a0c3154
fix(mobile): keep drafts when a thread is in the native CLI
cursoragent Aug 16, 2026
9e1d363
fix(web): show a disabled cursor while teleport is busy
cursoragent Aug 17, 2026
702df18
fix(teleport): keep discovery from mixing CLI homes and transcripts
cursoragent Aug 17, 2026
db4a2dc
fix(teleport): apply OpenCode cwd rules to explicit native paths
cursoragent Aug 17, 2026
4f7f53b
fix(teleport): drop Grok and OpenCode native session formats
cursoragent Aug 17, 2026
579dadc
fix(teleport): register native formats through an Effect service
cursoragent Aug 17, 2026
838953a
fix(teleport): probe native locks through ProcessRunner
cursoragent Aug 17, 2026
f700884
fix(opencode): wait for abort before accepting a new turn
cursoragent Aug 17, 2026
d75a839
fix(orchestration): prune only a thread's own attachments
cursoragent Aug 17, 2026
616a55e
fix(web): list physical projects for teleport import
cursoragent Aug 17, 2026
95c5b6e
fix(teleport): honor empty extra-instance home paths
cursoragent Aug 17, 2026
a5966af
fix(opencode): complete the abort gate with Effect 4 APIs
cursoragent Aug 17, 2026
26c6932
fix(opencode): emit one abort for concurrent Stop
cursoragent Aug 17, 2026
276620a
fix(teleport): import worktree threads back into their project
cursoragent Aug 17, 2026
f712cbd
fix(teleport): restore presence when export fails after pending
cursoragent Aug 17, 2026
84ea40b
fix(teleport): keep export cleanup from failing the original error
cursoragent Aug 17, 2026
5c6d9e1
fix(teleport): derive error messages and delete failed imports
cursoragent Aug 17, 2026
1fe0b03
fix(teleport): derive write errors from stage
cursoragent Aug 17, 2026
b65ca1a
fix(teleport): bind in-place imports before replacing history
cursoragent Aug 17, 2026
5328a96
Merge pingdotgg/t3code main into teleport tip.
cursoragent Aug 18, 2026
6c15543
fix(web): theme TeleportOutButton like other header controls
cursoragent Aug 19, 2026
f1ace6f
fix(web): keep teleported-out composer drafts unmutated
cursoragent Aug 19, 2026
710bd1c
Merge branch 'main' into cursor/teleport-opencode-1e23
baanish Aug 19, 2026
af47f2c
fix(teleport): commit in-place import as one T3 state transition
cursoragent Aug 19, 2026
49935d1
Merge branch 'main' into cursor/teleport-opencode-1e23
baanish Aug 19, 2026
aa2fad3
fix(teleport): revert importing fence on interrupt and defects
cursoragent Aug 19, 2026
535e79a
Merge branch 'main' into cursor/teleport-opencode-1e23
cursoragent Aug 19, 2026
6868b38
Merge branch 'main' into cursor/teleport-opencode-1e23
cursoragent Aug 20, 2026
40508c9
fix(web): keep locked composer drafts unmutated after async work
cursoragent Aug 20, 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
29 changes: 19 additions & 10 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type {
EnvironmentId,
MessageId,
ModelSelection,
OrchestrationThreadShell,
ProviderInteractionMode,
RuntimeMode,
ServerConfig as T3ServerConfig,
import {
isTeleportedOut,
teleportSendDisabledReason,
type EnvironmentId,
type MessageId,
type ModelSelection,
type OrchestrationThreadShell,
type ProviderInteractionMode,
type RuntimeMode,
type ServerConfig as T3ServerConfig,
} from "@t3tools/contracts";
import {
detectComposerTrigger,
Expand Down Expand Up @@ -290,10 +292,13 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer

const [previewImageUri, setPreviewImageUri] = useState<string | null>(null);
const hasContent = props.draftMessage.trim().length > 0 || props.draftAttachments.length > 0;
const teleportedOut = isTeleportedOut(props.selectedThread.teleport);
const teleportSendBlockReason = teleportSendDisabledReason(props.selectedThread.teleport);
// Opening and presentation count as active so the composer stays expanded
// while focus moves between its native editor and the settings picker.
const isExpanded = isFocused || settingsSheetPresentation.isActive;
const canSend = hasContent;
const canSend = hasContent && !teleportedOut;
const placeholder = teleportSendBlockReason ?? props.placeholder;

// Notify the parent from the derived value, not focus events: the parent
// sizes the feed inset from this, and blur-during-sheet would otherwise
Expand Down Expand Up @@ -537,6 +542,9 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
const { onChangeDraftMessage, onUpdateInteractionMode, draftMessage, onSendMessage } = props;

const handleSend = useCallback(async () => {
if (isTeleportedOut(props.selectedThread.teleport)) {
return;
}
const threadKey = scopedThreadKey(props.environmentId, props.selectedThread.id);
if (inFlightThreadIdsRef.current.has(threadKey)) return;
inFlightThreadIdsRef.current.add(threadKey);
Expand All @@ -560,6 +568,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
props.environmentLabel,
props.selectedThread.id,
props.selectedThread.title,
props.selectedThread.teleport,
]);
const handleCommandSelect = useCallback(
(item: ComposerCommandItem) => {
Expand Down Expand Up @@ -788,7 +797,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
onChangeText={props.onChangeDraftMessage}
onSelectionChange={handleSelectionChange}
onPasteImages={(uris) => void props.onNativePasteImages(uris)}
placeholder={props.placeholder}
placeholder={placeholder}
onFocus={handleFocus}
onBlur={handleBlur}
onSubmit={handleSend}
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/state/use-thread-composer-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo } from "react";

import {
CommandId,
isTeleportedOut,
MessageId,
type EnvironmentId,
type ModelSelection,
Expand Down Expand Up @@ -137,6 +138,9 @@ export function useThreadComposerState() {
const threadKey = scopedThreadKey(selectedThreadShell.environmentId, selectedThreadShell.id);
const draft = getComposerDraftSnapshot(threadKey);
const thread = selectedThreadDetail ?? selectedThreadShell;
if (isTeleportedOut(thread.teleport)) {
return null;
}
const text = draft.text.trim();
const attachments = draft.attachments;
if (text.length === 0 && attachments.length === 0) {
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.subscribeServerLifecycle]: AuthOrchestrationReadScope,
[WS_METHODS.subscribeAuthAccess]: AuthAccessReadScope,
[WS_METHODS.subscribeBackgroundPolicy]: AuthOrchestrationReadScope,
[WS_METHODS.teleportListSessions]: AuthOrchestrationReadScope,
[WS_METHODS.teleportImportSessions]: AuthOrchestrationOperateScope,
[WS_METHODS.teleportExportSession]: AuthOrchestrationOperateScope,
} as const satisfies Readonly<Record<WsRpcMethod, AuthEnvironmentScope>>;

export function requiredScopeForRpcMethod(method: string): AuthEnvironmentScope {
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
expect(second.capabilities.connectionProbe).toBe(true);
expect(second.capabilities.pullRequests).toBe(true);
expect(second.capabilities.threadTitleRegeneration).toBe(true);
expect(second.capabilities.teleport).toBe(true);
expect(second.capabilities.agentActivityPublishing).toBe(false);
}),
);
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export const make = Effect.gen(function* () {
threadPinning: true,
threadPinReorder: true,
threadTitleRegeneration: true,
teleport: true,
...(serverSelfUpdate === null ? {} : { serverSelfUpdate }),
...(serverSelfUpdate === "boot-service" ? { serverSelfUpdateProgress: true } : {}),
},
Expand Down
265 changes: 265 additions & 0 deletions apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1379,4 +1379,269 @@ describe("OrchestrationEngine", () => {

await system.dispose();
});

it("commits unarchive, T3 ownership, and history replacement together", async () => {
const system = await createOrchestrationSystem();
const { engine } = system;
const createdAt = now();
const projectId = asProjectId("project-teleport-import");
const threadId = ThreadId.make("thread-teleport-import");

await system.run(
engine.dispatch({
type: "project.create",
commandId: CommandId.make("cmd-teleport-import-project"),
projectId,
title: "Teleport Import",
workspaceRoot: "/tmp/project-teleport-import",
defaultModelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
}),
);
await system.run(
engine.dispatch({
type: "thread.create",
commandId: CommandId.make("cmd-teleport-import-thread"),
threadId,
projectId,
title: "Old title",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "full-access",
branch: null,
worktreePath: null,
createdAt,
}),
);
await system.run(
engine.dispatch({
type: "thread.teleport.set",
commandId: CommandId.make("cmd-teleport-import-native"),
threadId,
teleport: {
presence: "native",
provider: "codex",
externalSessionId: "session-import",
nativePath: "/tmp/session.jsonl",
lastSyncedAt: createdAt,
},
createdAt,
}),
);
await system.run(
engine.dispatch({
type: "thread.history.replace",
commandId: CommandId.make("cmd-teleport-import-old-history"),
threadId,
messages: [
{
id: asMessageId("old-1"),
role: "user",
text: "old",
turnId: null,
streaming: false,
createdAt,
updatedAt: createdAt,
},
],
createdAt,
}),
);
await system.run(
engine.dispatch({
type: "thread.archive",
commandId: CommandId.make("cmd-teleport-import-archive"),
threadId,
}),
);
await system.run(
engine.dispatch({
type: "thread.teleport.import",
commandId: CommandId.make("cmd-teleport-import-commit"),
threadId,
teleport: {
presence: "t3",
provider: "codex",
externalSessionId: "session-import",
nativePath: "/tmp/session.jsonl",
lastSyncedAt: createdAt,
},
messages: [
{
id: asMessageId("imported-1"),
role: "user",
text: "imported",
turnId: null,
streaming: false,
createdAt,
updatedAt: createdAt,
},
],
createdAt,
}),
);

const readModel = await system.readModel();
const thread = readModel.threads.find((candidate) => candidate.id === threadId);
expect(thread?.archivedAt).toBeNull();
expect(thread?.teleport?.presence).toBe("t3");
expect(thread?.messages.map((message) => message.text)).toEqual(["imported"]);

await system.run(
engine.dispatch({
type: "thread.teleport.import",
commandId: CommandId.make("cmd-teleport-import-retry"),
threadId,
teleport: {
presence: "t3",
provider: "codex",
externalSessionId: "session-import",
nativePath: "/tmp/session.jsonl",
lastSyncedAt: createdAt,
},
messages: [
{
id: asMessageId("imported-1"),
role: "user",
text: "imported",
turnId: null,
streaming: false,
createdAt,
updatedAt: createdAt,
},
],
createdAt,
}),
);
const retried = await system.readModel();
const retriedThread = retried.threads.find((candidate) => candidate.id === threadId);
expect(retriedThread?.teleport?.presence).toBe("t3");
expect(retriedThread?.messages.map((message) => message.text)).toEqual(["imported"]);

await system.dispose();
});

it("rejects turn start against importing presence and recovers to native", async () => {
const system = await createOrchestrationSystem();
const { engine } = system;
const createdAt = now();
const projectId = asProjectId("project-teleport-importing");
const threadId = ThreadId.make("thread-teleport-importing");

await system.run(
engine.dispatch({
type: "project.create",
commandId: CommandId.make("cmd-importing-project"),
projectId,
title: "Importing",
workspaceRoot: "/tmp/project-teleport-importing",
defaultModelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
}),
);
await system.run(
engine.dispatch({
type: "thread.create",
commandId: CommandId.make("cmd-importing-thread"),
threadId,
projectId,
title: "Thread",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "full-access",
branch: null,
worktreePath: null,
createdAt,
}),
);
await system.run(
engine.dispatch({
type: "thread.teleport.set",
commandId: CommandId.make("cmd-importing-fence"),
threadId,
teleport: {
presence: "importing",
provider: "codex",
externalSessionId: "session-importing",
nativePath: "/tmp/session.jsonl",
lastSyncedAt: createdAt,
restorePresence: "native",
},
createdAt,
}),
);

await expect(
system.run(
engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.make("cmd-importing-turn"),
threadId,
message: {
messageId: asMessageId("msg-importing"),
role: "user",
text: "hello",
attachments: [],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "full-access",
createdAt,
}),
),
).rejects.toThrow("being imported");

await system.run(
engine.dispatch({
type: "thread.teleport.set",
commandId: CommandId.make("cmd-importing-recover"),
threadId,
teleport: {
presence: "native",
provider: "codex",
externalSessionId: "session-importing",
nativePath: "/tmp/session.jsonl",
lastSyncedAt: createdAt,
},
createdAt,
}),
);

const recovered = await system.readModel();
const thread = recovered.threads.find((candidate) => candidate.id === threadId);
expect(thread?.teleport?.presence).toBe("native");
expect(thread?.teleport?.restorePresence).toBeUndefined();

await expect(
system.run(
engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.make("cmd-importing-turn-after-recover"),
threadId,
message: {
messageId: asMessageId("msg-importing-after"),
role: "user",
text: "hello",
attachments: [],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "full-access",
createdAt,
}),
),
).rejects.toThrow("native CLI");

await system.dispose();
});
});
Loading
Loading