Repository navigation
refactor(web): open and close thread terminals through one hook #20289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ import { | |
| import { useLoadBalancedEnvironment } from "../hooks/useLoadBalancedEnvironment"; | ||
| import { useScratchProject } from "../hooks/useScratchProject"; | ||
| import { useStableCallback } from "../hooks/useStableCallback"; | ||
| import { useThreadTerminalActions } from "./terminal/useThreadTerminalActions"; | ||
| import { usePendingThreadRequests } from "./chat/usePendingThreadRequests"; | ||
| import { isScratchProject } from "@t3tools/client-runtime/state/projects"; | ||
| import { visibleThreadPullRequests } from "@t3tools/shared/threadPullRequests"; | ||
|
|
@@ -76,7 +77,6 @@ import { | |
| resolveEnvironmentMachineKind, | ||
| resolveProjectProviderInstanceEnabled, | ||
| RuntimeMode, | ||
| TerminalOpenInput, | ||
| type WorktreeSetupSnapshot, | ||
| } from "@t3tools/contracts"; | ||
| import { type EnvironmentConnectionPresentation } from "@t3tools/client-runtime/connection"; | ||
|
|
@@ -125,11 +125,7 @@ import { | |
| resolvePromptInjectedEffort, | ||
| resolveSelectableModel, | ||
| } from "@t3tools/shared/model"; | ||
| import { | ||
| projectScriptCwd, | ||
| projectScriptRuntimeEnv, | ||
| resolveProjectScripts, | ||
| } from "@t3tools/shared/projectScripts"; | ||
| import { projectScriptCwd, resolveProjectScripts } from "@t3tools/shared/projectScripts"; | ||
| import { CHAT_LIST_ANCHOR_OFFSET } from "@t3tools/shared/chatList"; | ||
| import { derivePendingBackgroundWork } from "@t3tools/shared/orchestrationV2PendingBackgroundWork"; | ||
| import { | ||
|
|
@@ -140,7 +136,7 @@ import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; | |
| import { sourceControlRepositorySelector } from "@t3tools/shared/sourceControl"; | ||
| import { truncate } from "@t3tools/shared/String"; | ||
| import { resolveThreadReferenceCopyTarget } from "@t3tools/shared/threadReference"; | ||
| import { nextTerminalId, resolveTerminalSessionLabel } from "@t3tools/shared/terminalLabels"; | ||
| import { resolveTerminalSessionLabel } from "@t3tools/shared/terminalLabels"; | ||
| import { Debouncer } from "@tanstack/react-pacer"; | ||
| import { useAtomValue } from "@effect/atom-react"; | ||
| import { Atom } from "effect/reactivity"; | ||
|
|
@@ -400,7 +396,6 @@ import { | |
| primaryServerKeybindingsAtom, | ||
| serverEnvironment, | ||
| } from "../state/server"; | ||
| import { terminalEnvironment } from "../state/terminal"; | ||
| import { threadEnvironment } from "../state/threads"; | ||
| import { workspacePreparationRetryRunIds } from "@t3tools/client-runtime/state/turn-item-presentation"; | ||
| import { resolveProviderSkillsForCwd } from "@t3tools/client-runtime/providerSkills"; | ||
|
|
@@ -1025,9 +1020,6 @@ export default function ChatView(props: ChatViewProps) { | |
| const removeKeybinding = useAtomCommand(serverEnvironment.removeKeybinding, { | ||
| reportFailure: false, | ||
| }); | ||
| const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open"); | ||
| const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); | ||
| const closeTerminalMutation = useAtomCommand(terminalEnvironment.close, "terminal close"); | ||
| const createThread = useOrchestrationCommand(threadEnvironment.create, { reportFailure: false }); | ||
| const deleteThread = useOrchestrationCommand(threadEnvironment.delete, { reportFailure: false }); | ||
| const updateThreadMetadata = useOrchestrationCommand(threadEnvironment.updateMetadata, { | ||
|
|
@@ -1602,6 +1594,7 @@ export default function ChatView(props: ChatViewProps) { | |
| return labels; | ||
| }, [activeThreadKnownSessions]); | ||
| const activeThreadRef = useActiveThreadRef(activeThread); | ||
| const terminalActions = useThreadTerminalActions(activeThreadRef); | ||
| const activeThreadKey = activeThreadRef ? scopedThreadKey(activeThreadRef) : null; | ||
| const activeEnvironmentServerBrowser = useEnvironmentSupportsServerBrowser( | ||
| activeThreadRef?.environmentId ?? null, | ||
|
|
@@ -1690,18 +1683,7 @@ export default function ChatView(props: ChatViewProps) { | |
| ), | ||
| [rightPanelState.surfaces], | ||
| ); | ||
| const allocatableActiveTerminalIds = useMemo( | ||
| () => [...new Set([...activeKnownTerminalIds, ...panelTerminalIds])], | ||
| [activeKnownTerminalIds, panelTerminalIds], | ||
| ); | ||
| const canReuseTerminal = activeThreadKnownSessionsRaw !== null; | ||
| const allocateTerminalId = () => | ||
| nextTerminalId( | ||
| allocatableActiveTerminalIds, | ||
| canReuseTerminal && readEnvironmentScope(environmentId, AuthTerminalReadScope) | ||
| ? undefined | ||
| : randomUUID(), | ||
| ); | ||
| const previewPanelOpen = activeRightPanelKind === "preview" && browserAvailable; | ||
| const rightPanelOpen = rightPanelState.isOpen; | ||
| const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } = | ||
|
|
@@ -3871,119 +3853,40 @@ export default function ChatView(props: ChatViewProps) { | |
| storeEnsureTerminal(activeThreadRef, existingTerminalId, { open: true, active: true }); | ||
| return; | ||
| } | ||
| if (!activeThreadId || !activeProject) { | ||
| return; | ||
| } | ||
| const cwdForOpen = gitCwd ?? activeProject.workspaceRoot; | ||
| if (!cwdForOpen) { | ||
| return; | ||
| } | ||
| const terminalId = allocateTerminalId(); | ||
| storeEnsureTerminal(activeThreadRef, terminalId, { open: true }); | ||
| void openTerminal({ | ||
| environmentId, | ||
| input: { | ||
| threadId: activeThreadId, | ||
| terminalId, | ||
| cwd: cwdForOpen, | ||
| ...(activeThreadWorktreePath != null ? { worktreePath: activeThreadWorktreePath } : {}), | ||
| env: projectScriptRuntimeEnv({ | ||
| project: { cwd: activeProject.workspaceRoot }, | ||
| worktreePath: activeThreadWorktreePath, | ||
| }), | ||
| }, | ||
| }); | ||
| void terminalActions.launchTerminal({ | ||
| place: (terminalId) => storeEnsureTerminal(activeThreadRef, terminalId, { open: true }), | ||
| })?.result; | ||
| return; | ||
| } | ||
| setTerminalOpen(nextOpen); | ||
| }; | ||
| const toggleTerminalVisibilityStable = useStableCallback(toggleTerminalVisibility); | ||
| const splitTerminal = (direction: "horizontal" | "vertical" = "horizontal") => { | ||
| if ( | ||
| !hasTerminalWriteAccess() || | ||
| !activeThreadRef || | ||
| hasReachedSplitLimit || | ||
| !activeThreadId || | ||
| !activeProject | ||
| ) { | ||
| return; | ||
| } | ||
| const cwdForOpen = gitCwd ?? activeProject.workspaceRoot; | ||
| if (!cwdForOpen) { | ||
| return; | ||
| } | ||
| const terminalId = allocateTerminalId(); | ||
| if (direction === "vertical") { | ||
| storeSplitTerminalVertical(activeThreadRef, terminalId); | ||
| } else { | ||
| storeSplitTerminal(activeThreadRef, terminalId); | ||
| } | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
| void openTerminal({ | ||
| environmentId, | ||
| input: { | ||
| threadId: activeThreadId, | ||
| terminalId, | ||
| cwd: cwdForOpen, | ||
| ...(activeThreadWorktreePath != null ? { worktreePath: activeThreadWorktreePath } : {}), | ||
| env: projectScriptRuntimeEnv({ | ||
| project: { cwd: activeProject.workspaceRoot }, | ||
| worktreePath: activeThreadWorktreePath, | ||
| }), | ||
| }, | ||
| if (!activeThreadRef || hasReachedSplitLimit) return; | ||
| const launched = terminalActions.launchTerminal({ | ||
| place: (terminalId) => | ||
| direction === "vertical" | ||
| ? storeSplitTerminalVertical(activeThreadRef, terminalId) | ||
| : storeSplitTerminal(activeThreadRef, terminalId), | ||
| }); | ||
| if (!launched) return; | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
| void launched.result; | ||
| }; | ||
| const splitTerminalStable = useStableCallback(splitTerminal); | ||
| const createNewTerminal = () => { | ||
| if (!hasTerminalWriteAccess() || !activeThreadRef || !activeThreadId || !activeProject) { | ||
| return; | ||
| } | ||
| const cwdForOpen = gitCwd ?? activeProject.workspaceRoot; | ||
| if (!cwdForOpen) { | ||
| return; | ||
| } | ||
| const terminalId = allocateTerminalId(); | ||
| storeNewTerminal(activeThreadRef, terminalId); | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
| void openTerminal({ | ||
| environmentId, | ||
| input: { | ||
| threadId: activeThreadId, | ||
| terminalId, | ||
| cwd: cwdForOpen, | ||
| ...(activeThreadWorktreePath != null ? { worktreePath: activeThreadWorktreePath } : {}), | ||
| env: projectScriptRuntimeEnv({ | ||
| project: { cwd: activeProject.workspaceRoot }, | ||
| worktreePath: activeThreadWorktreePath, | ||
| }), | ||
| }, | ||
| if (!activeThreadRef) return; | ||
| const launched = terminalActions.launchTerminal({ | ||
| place: (terminalId) => storeNewTerminal(activeThreadRef, terminalId), | ||
| }); | ||
| if (!launched) return; | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
| void launched.result; | ||
| }; | ||
| const createNewTerminalStable = useStableCallback(createNewTerminal); | ||
| const closeTerminal = (terminalId: string) => { | ||
| if (!hasTerminalWriteAccess() || !activeThreadId || !activeThreadRef) return; | ||
| const fallbackExitWrite = () => | ||
| writeTerminal({ | ||
| environmentId, | ||
| input: { threadId: activeThreadId, terminalId, data: "exit\n" }, | ||
| }); | ||
| void (async () => { | ||
| const closeResult = await closeTerminalMutation({ | ||
| environmentId, | ||
| input: { | ||
| threadId: activeThreadId, | ||
| terminalId, | ||
| deleteHistory: true, | ||
| }, | ||
| }); | ||
| if ( | ||
| closeResult._tag === "Failure" && | ||
| !isAtomCommandInterrupted(closeResult) && | ||
| hasTerminalWriteAccess() | ||
| ) { | ||
| await fallbackExitWrite(); | ||
| } | ||
| })(); | ||
| if (!hasTerminalWriteAccess() || !activeThreadRef) return; | ||
| terminalActions.closeTerminalSession(terminalId); | ||
| storeCloseTerminal(activeThreadRef, terminalId); | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
| }; | ||
|
|
@@ -4026,39 +3929,20 @@ export default function ChatView(props: ChatViewProps) { | |
| } | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
|
|
||
| const runtimeEnv = projectScriptRuntimeEnv({ | ||
| project: { | ||
| cwd: activeProject.workspaceRoot, | ||
| }, | ||
| worktreePath: targetWorktreePath, | ||
| const launched = terminalActions.launchTerminal({ | ||
| place: (terminalId) => | ||
| shouldCreateNewTerminal | ||
| ? storeNewTerminal(activeThreadRef, terminalId) | ||
| : storeSetActiveTerminal(activeThreadRef, terminalId), | ||
| ...(shouldCreateNewTerminal | ||
| ? { size: { cols: SCRIPT_TERMINAL_COLS, rows: SCRIPT_TERMINAL_ROWS } } | ||
| : { terminalId: baseTerminalId }), | ||
| location: { cwd: targetCwd, worktreePath: targetWorktreePath }, | ||
| ...(options?.env ? { extraEnv: options.env } : {}), | ||
| }); | ||
| const targetTerminalId = shouldCreateNewTerminal ? allocateTerminalId() : baseTerminalId; | ||
| const openTerminalInput: TerminalOpenInput = shouldCreateNewTerminal | ||
| ? { | ||
| threadId: activeThreadId, | ||
| terminalId: targetTerminalId, | ||
| cwd: targetCwd, | ||
| ...(targetWorktreePath !== null ? { worktreePath: targetWorktreePath } : {}), | ||
| env: runtimeEnv, | ||
| cols: SCRIPT_TERMINAL_COLS, | ||
| rows: SCRIPT_TERMINAL_ROWS, | ||
| } | ||
| : { | ||
| threadId: activeThreadId, | ||
| terminalId: targetTerminalId, | ||
| cwd: targetCwd, | ||
| ...(targetWorktreePath !== null ? { worktreePath: targetWorktreePath } : {}), | ||
| env: runtimeEnv, | ||
| }; | ||
|
|
||
| if (shouldCreateNewTerminal) { | ||
| storeNewTerminal(activeThreadRef, targetTerminalId); | ||
| } else { | ||
| storeSetActiveTerminal(activeThreadRef, targetTerminalId); | ||
| } | ||
|
|
||
| const openResult = await openTerminal({ environmentId, input: openTerminalInput }); | ||
| if (!launched) return; | ||
| const targetTerminalId = launched.terminalId; | ||
| const openResult = await launched.result; | ||
| if (openResult._tag === "Failure") { | ||
| if (!isAtomCommandInterrupted(openResult)) { | ||
| const error = squashAtomCommandFailure(openResult); | ||
|
|
@@ -4070,15 +3954,11 @@ export default function ChatView(props: ChatViewProps) { | |
| return; | ||
| } | ||
|
|
||
| if (!hasTerminalWriteAccess()) return; | ||
| const writeResult = await writeTerminal({ | ||
| environmentId, | ||
| input: { | ||
| threadId: activeThreadId, | ||
| terminalId: targetTerminalId, | ||
| data: `${script.command}\r`, | ||
| }, | ||
| }); | ||
| const writeResult = await terminalActions.writeToTerminal( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π High If the user switches from thread A to B while Also found in 1 other location(s)
π Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| targetTerminalId, | ||
| `${script.command}\r`, | ||
| ); | ||
| if (!writeResult) return; | ||
| if (writeResult._tag === "Failure") { | ||
| if (!isAtomCommandInterrupted(writeResult)) { | ||
| const error = squashAtomCommandFailure(writeResult); | ||
|
|
@@ -4846,56 +4726,34 @@ export default function ChatView(props: ChatViewProps) { | |
| }; | ||
| const togglePreviewPanelStable = useStableCallback(togglePreviewPanel); | ||
| const addTerminalSurface = () => { | ||
| if (!hasTerminalWriteAccess() || !activeThreadRef || !activeThreadId || !activeProject) return; | ||
| const cwd = gitCwd ?? activeProject.workspaceRoot; | ||
| const terminalId = allocateTerminalId(); | ||
| useRightPanelStore.getState().openTerminal(activeThreadRef, terminalId); | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
| void openTerminal({ | ||
| environmentId: activeThreadRef.environmentId, | ||
| input: { | ||
| threadId: activeThreadId, | ||
| terminalId, | ||
| cwd, | ||
| ...(activeThreadWorktreePath != null ? { worktreePath: activeThreadWorktreePath } : {}), | ||
| env: projectScriptRuntimeEnv({ | ||
| project: { cwd: activeProject.workspaceRoot }, | ||
| worktreePath: activeThreadWorktreePath, | ||
| }), | ||
| }, | ||
| if (!activeThreadRef) return; | ||
| const launched = terminalActions.launchTerminal({ | ||
| place: (terminalId) => | ||
| useRightPanelStore.getState().openTerminal(activeThreadRef, terminalId), | ||
| }); | ||
| if (!launched) return; | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
| void launched.result; | ||
| }; | ||
| const addTerminalSurfaceStable = useStableCallback(addTerminalSurface); | ||
| const splitPanelTerminal = (direction: "horizontal" | "vertical" = "horizontal") => { | ||
| if ( | ||
| !hasTerminalWriteAccess() || | ||
| !activeThreadRef || | ||
| !activeThreadId || | ||
| !activeProject || | ||
| activeRightPanelSurface?.kind !== "terminal" || | ||
| activeRightPanelSurface.terminalIds.length >= MAX_TERMINALS_PER_GROUP | ||
| ) { | ||
| return; | ||
| } | ||
| const terminalId = allocateTerminalId(); | ||
| const cwd = gitCwd ?? activeProject.workspaceRoot; | ||
| useRightPanelStore | ||
| .getState() | ||
| .splitTerminal(activeThreadRef, activeRightPanelSurface.id, terminalId, direction); | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
| void openTerminal({ | ||
| environmentId: activeThreadRef.environmentId, | ||
| input: { | ||
| threadId: activeThreadId, | ||
| terminalId, | ||
| cwd, | ||
| ...(activeThreadWorktreePath != null ? { worktreePath: activeThreadWorktreePath } : {}), | ||
| env: projectScriptRuntimeEnv({ | ||
| project: { cwd: activeProject.workspaceRoot }, | ||
| worktreePath: activeThreadWorktreePath, | ||
| }), | ||
| }, | ||
| const surfaceId = activeRightPanelSurface.id; | ||
| const launched = terminalActions.launchTerminal({ | ||
| place: (terminalId) => | ||
| useRightPanelStore | ||
| .getState() | ||
| .splitTerminal(activeThreadRef, surfaceId, terminalId, direction), | ||
| }); | ||
| if (!launched) return; | ||
| setTerminalFocusRequestId((value) => value + 1); | ||
| void launched.result; | ||
| }; | ||
| const splitPanelTerminalStable = useStableCallback(splitPanelTerminal); | ||
| const splitPanelTerminalVertical = () => { | ||
|
|
@@ -4917,10 +4775,7 @@ export default function ChatView(props: ChatViewProps) { | |
| activeRightPanelSurface?.kind !== "terminal" | ||
| ) | ||
| return; | ||
| void closeTerminalMutation({ | ||
| environmentId: activeThreadRef.environmentId, | ||
| input: { threadId: activeThreadRef.threadId, terminalId, deleteHistory: true }, | ||
| }); | ||
| terminalActions.closeTerminalSession(terminalId); | ||
| storeCloseTerminal(activeThreadRef, terminalId); | ||
| useRightPanelStore | ||
| .getState() | ||
|
|
@@ -5004,10 +4859,7 @@ export default function ChatView(props: ChatViewProps) { | |
| ) { | ||
| for (const terminalId of surface.terminalIds) { | ||
| storeCloseTerminal(activeThreadRef, terminalId); | ||
| void closeTerminalMutation({ | ||
| environmentId: activeThreadRef.environmentId, | ||
| input: { threadId: activeThreadRef.threadId, terminalId, deleteHistory: true }, | ||
| }); | ||
| terminalActions.closeTerminalSession(terminalId); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
4859
to
4865
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π― Functional Correctness | π‘ Minor | β‘ Quick win π Supported by static analysisπ Script executed: sed -n '4830,4930p' apps/web/src/components/ChatView.tsx
sed -n '120,150p' apps/web/src/components/terminal/useThreadTerminalActions.ts
rg -n 'finishRightPanelSurfaceClose|cleanupRightPanelSurfaces|closeSurfaces|confirm.*Surface' apps/web/src/components/ChatView.tsxRepository: pingdotgg/t3code Length of output: 5585 π Script executed: set -o pipefail
printf '%s\n' '--- current right-panel close callers ---'
sed -n '4908,4975p' apps/web/src/components/ChatView.tsx
printf '%s\n' '--- current hook call sites and declaration context ---'
rg -n -F -- 'useThreadTerminalActions' 'terminalActions.closeTerminalSession' apps/web/src/components/ChatView.tsx apps/web/src/components/PersistentThreadTerminals.tsx apps/web/src/components/terminal
printf '%s\n' '--- base right-panel cleanup/close flow ---'
git show 1676a5fc36453c2d33fa12fab5d984990b66d62d:apps/web/src/components/ChatView.tsx | sed -n '3945,4015p'Repository: pingdotgg/t3code Length of output: 6712 π Script executed: set -o pipefail
sed -n '1,175p' apps/web/src/components/terminal/useThreadTerminalActions.tsRepository: pingdotgg/t3code Length of output: 7001 Preserve the thread reference for delayed terminal closes. A terminal close confirmation can resolve after the user switches threads. The retained cleanup callback uses the old Pass the target thread reference to the close action and use it for both the close and fallback write. Suggested fix- terminalActions.closeTerminalSession(terminalId);
+ terminalActions.closeTerminalSession(terminalId, activeThreadRef);- const closeTerminalSession = (terminalId: string) => {
- if (!threadRef || !canWrite()) return;
- const environmentId = threadRef.environmentId;
- const threadId = threadRef.threadId;
+ const closeTerminalSession = (
+ terminalId: string,
+ targetThreadRef: ScopedThreadRef | null = threadRef,
+ ) => {
+ if (
+ !targetThreadRef ||
+ !readEnvironmentScope(targetThreadRef.environmentId, AuthTerminalOperateScope)
+ )
+ return;
+ const environmentId = targetThreadRef.environmentId;
+ const threadId = targetThreadRef.threadId;π€ Prompt for AI Agents |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π High
components/ChatView.tsx:4029After confirming a close requested for thread A, the code can close the matching terminal in thread B while removing Aβs terminal from the drawer. The confirmed callback retains Aβs
activeThreadRefforstoreCloseTerminal, butterminalActions.closeTerminalSessionresolves the latest thread and closes it withdeleteHistory: true; bind both operations to the thread whose terminal was confirmed.π Reply "fix it for me" or copy this AI Prompt for your agent: