Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This is a personal fork of [pingdotgg/t3code](https://github.com/pingdotgg/t3cod

- **Indefinite snooze** — an "Until I wake it" preset in the thread sidebar snooze menus parks a thread on the snoozed shelf with no timer: it comes back only when explicitly woken or when you send it a message. Indefinite rows sort after every timed wake and carry a "parked" label; the T3 CLI reports them as `Snoozed: until woken`.
- **Conversation forking** — fork a Codex or Claude thread mid-conversation into a new thread, including immediately after importing an external session; forked threads are titled with a 🔱 prefix.
- **Two-pane split view** — web and desktop can show a second thread beside the routed one, including a thread from a different environment. Open it from the titlebar button or the ⌘K "Open thread in split view..." action; the secondary pane carries its own switch and close controls, the URL and sidebar keep driving only the main pane, and opening the secondary thread in the main pane folds the split. A ring marks the active pane, a rebindable shortcut (default mod+\) jumps between panes, and window shortcuts — including permission-prompt number keys — act only on the active pane. While split, each pane's right panel opens as an overlay sheet, terminals stay with the pane that owns their thread, the divider drags and remembers its position, and actions that create a new thread (new thread, implement plan) stay in the main pane.
- **Mobile swipe-right actions** — swiping a thread row right reveals Pin (Unpin on a pinned row), Fork, and Archive, in that order. Each action only appears where it applies — pin needs a server that supports pinning, fork needs a forkable Codex or Claude conversation — and every one of them has a twin in the row's long-press menu. Pin and Archive keep you on the list; Fork opens the new copy. Archive stays innermost, so a full swipe right still archives.
- **Session import** — import external Claude Code and Codex CLI sessions as native T3 threads from thread-sidebar project settings (choosing the environment when the project is grouped across several) or the desktop project context menu, including strict resume/continuation, Claude-generated and explicit CLI session titles, and explicit instance labels when more than one provider instance can import sessions. Sessions already owned by a T3 thread stay visible as linked candidates; importing one again forks the provider history into a fresh continuation so the original thread keeps sole ownership of its native session.
- **Archived-thread recents, search, grouping & undo** — the final thread-sidebar section on desktop and mobile keeps a configurable recent archive close at hand, with one-action restore, archive-aware menus, and automatic restore when a new message is sent; the full archive remains searchable, grouped, and filterable by project. The open web thread can be archived from the command palette or a rebindable shortcut (default mod+shift+E), a mobile thread row archives with a full swipe right, and on web Cmd+Z outside the composer reverses the most recent archive or snooze, whichever happened last — an empty new-thread screen reopens the restored thread, while another active conversation stays in place. The section folds on every client and starts folded, since archived threads are the ones you deliberately put away; the header then reports the archive count, and the thread you have open keeps its row while the section is folded.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/commandPaletteBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const COMMAND_PALETTE_OPEN_EVENT = "t3code:open-command-palette";

export interface CommandPaletteOpenDetail {
readonly open?: "add-project" | "new-thread-in";
readonly open?: "add-project" | "new-thread-in" | "open-in-split";
}

export function openCommandPalette(detail?: CommandPaletteOpenDetail): void {
Expand Down
14 changes: 11 additions & 3 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1374,7 +1374,12 @@ function ChatMarkdown({
reportFailure: false,
});
const preparedConnection = usePreparedConnection(threadRef?.environmentId ?? null);
const environmentId = useActiveEnvironmentId();
const activeEnvironmentId = useActiveEnvironmentId();
// The thread's own environment must win: the singleton "active" environment
// tracks the routed thread, which is the wrong server for markdown rendered
// in the secondary split pane (file links would open via another machine's
// editor and RPC).
const environmentId = threadRef?.environmentId ?? activeEnvironmentId;
const serverConfig = useAtomValue(serverEnvironment.configValueAtom(environmentId));
const openInPreferredEditor = useOpenInPreferredEditor(
environmentId,
Expand Down Expand Up @@ -1478,8 +1483,11 @@ function ChatMarkdown({
(workspaceRelativePath: string, line: number | undefined) => {
if (!threadRef) return;
// Claimed on every open so a synchronous one supersedes a lookup already
// in flight.
const isLatestLookup = claimWorkspaceBasenameLookup();
// in flight. Scoped per thread so a click in one split pane cannot
// cancel the other pane's lookup.
const isLatestLookup = claimWorkspaceBasenameLookup(
`${threadRef.environmentId}:${threadRef.threadId}`,
);
const openAt = (path: string) =>
useRightPanelStore.getState().openFile(threadRef, path, line);
if (!cwd || !needsWorkspaceBasenameLookup(workspaceRelativePath)) {
Expand Down
15 changes: 9 additions & 6 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
type EnvironmentId,
isProviderDriverKind,
ProjectId,
type ModelSelection,
type OrchestrationSessionStatus,
type ProviderDriverKind,
Expand All @@ -24,12 +23,15 @@ import {
import type { DraftThreadEnvMode } from "../composerDraftStore";
import type { ComposerSubmissionIntent } from "../composer-logic";

export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";
// v2: keyed by scoped `environmentId:projectId`. Bare project ids collide
// across environments (cloned state), so v1 entries were ambiguous and are
// deliberately abandoned rather than migrated.
export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project-v2";
export const MAX_HIDDEN_MOUNTED_TERMINAL_THREADS = 10;
export const MAX_HIDDEN_MOUNTED_PREVIEW_THREADS = 3;
export const ENVIRONMENT_RECONNECT_WARNING_GRACE_MS = 2_000;

export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String);
export const LastInvokedScriptByProjectSchema = Schema.Record(Schema.String, Schema.String);

export function shouldDockDraftHeroForSubmission(input: {
isDraftHeroState: boolean;
Expand Down Expand Up @@ -462,13 +464,14 @@ export function buildExpiredTerminalContextToastCopy(
}

export function branchMismatchKey(
threadId: string | null,
/** Scoped `environmentId:threadId` key — bare ids collide across environments. */
threadKey: string | null,
mismatch: { threadBranch: string; currentBranch: string } | null,
): string | null {
if (!threadId || !mismatch) {
if (!threadKey || !mismatch) {
return null;
}
return `${threadId}:${mismatch.threadBranch}:${mismatch.currentBranch}`;
return `${threadKey}:${mismatch.threadBranch}:${mismatch.currentBranch}`;
}

// The mismatch banner only matters when the user is about to send: passive
Expand Down
Loading