Skip to content
76 changes: 75 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,24 @@ import {
} from "@app/orchestration/useWorkspaceOrchestration";
import { useAppShellOrchestration } from "@app/orchestration/useLayoutOrchestration";
import { buildCodexArgsOptions } from "@threads/utils/codexArgsProfiles";
import { clampThreadName } from "@threads/utils/threadNaming";
import { normalizeCodexArgsInput } from "@/utils/codexArgsInput";
import {
resolveWorkspaceRuntimeCodexArgsBadgeLabel,
resolveWorkspaceRuntimeCodexArgsOverride,
} from "@threads/utils/threadCodexParamsSeed";
import { setWorkspaceRuntimeCodexArgs } from "@services/tauri";
import { generateRunMetadata, setWorkspaceRuntimeCodexArgs } from "@services/tauri";

const MAX_THREAD_TITLE_PROMPT_CHARS = 1200;

function cleanThreadTitlePrompt(text: string) {
const withoutImages = text.replace(/\[image(?: x\d+)?\]/gi, " ");
const withoutSkills = withoutImages.replace(/(^|\s)\$[A-Za-z0-9_-]+(?=\s|$)/g, " ");
const normalized = withoutSkills.replace(/\s+/g, " ").trim();
return normalized.length > MAX_THREAD_TITLE_PROMPT_CHARS
? normalized.slice(0, MAX_THREAD_TITLE_PROMPT_CHARS)
: normalized;
}

const AboutView = lazy(() =>
import("@/features/about/components/AboutView").then((module) => ({
Expand Down Expand Up @@ -1545,6 +1557,65 @@ function MainApp() {
recentThreadsUpdatedAt: updatedAt > 0 ? updatedAt : null,
};
}, [activeWorkspaceId, threadsByWorkspace]);
const activeThreadSummary = useMemo(() => {
if (!activeWorkspaceId || !activeThreadId) {
return null;
}
const threads = threadsByWorkspace[activeWorkspaceId] ?? [];
return threads.find((thread) => thread.id === activeThreadId) ?? null;
}, [activeThreadId, activeWorkspaceId, threadsByWorkspace]);
const activeThreadInfo = useMemo(() => {
if (!activeWorkspace || !activeThreadId) {
return null;
}
return {
threadId: activeThreadId,
name: activeThreadSummary?.name?.trim() || "Untitled thread",
projectDir: activeWorkspace.path,
branchName: gitStatus.branchName || "unknown",
createdAt: activeThreadSummary?.createdAt ?? null,
updatedAt: activeThreadSummary?.updatedAt ?? null,
modelId: activeThreadSummary?.modelId ?? null,
effort: activeThreadSummary?.effort ?? null,
tokenUsage: activeTokenUsage,
};
}, [
activeThreadId,
activeThreadSummary,
activeWorkspace,
activeTokenUsage,
gitStatus.branchName,
]);
const handleHeaderRenameActiveThreadName = useCallback(
(name: string) => {
if (!activeWorkspaceId || !activeThreadId) {
return;
}
renameThread(activeWorkspaceId, activeThreadId, name);
},
[activeThreadId, activeWorkspaceId, renameThread],
);
const handleGenerateActiveThreadName = useCallback(async () => {
if (!activeWorkspaceId || !activeThreadId) {
return null;
}
let prompt = "";
for (const item of activeItems) {
if (item.kind !== "message" || item.role !== "user") {
continue;
}
const candidatePrompt = cleanThreadTitlePrompt(item.text);
if (candidatePrompt) {
prompt = candidatePrompt;
break;
}
}
if (!prompt) {
return null;
}
const metadata = await generateRunMetadata(activeWorkspaceId, prompt);
return clampThreadName(metadata.title ?? "");
}, [activeItems, activeThreadId, activeWorkspaceId]);
const {
content: agentMdContent,
exists: agentMdExists,
Expand Down Expand Up @@ -2193,6 +2264,9 @@ function MainApp() {
handleCheckoutPullRequest(pullRequest.number),
onCreateBranch: handleCreateBranch,
onCopyThread: handleCopyThread,
activeThreadInfo,
onRenameActiveThreadName: handleHeaderRenameActiveThreadName,
onGenerateActiveThreadName: handleGenerateActiveThreadName,
onToggleTerminal: handleToggleTerminalWithFocus,
showTerminalButton: !isCompact,
showWorkspaceTools: !isCompact,
Expand Down
44 changes: 44 additions & 0 deletions src/features/app/components/MainHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "react";
import Check from "lucide-react/dist/esm/icons/check";
import Copy from "lucide-react/dist/esm/icons/copy";
import Info from "lucide-react/dist/esm/icons/info";
import Terminal from "lucide-react/dist/esm/icons/terminal";
import { revealItemInDir } from "@tauri-apps/plugin-opener";
import type { BranchInfo, OpenAppTarget, WorkspaceInfo } from "../../../types";
Expand All @@ -18,6 +19,10 @@ import { LaunchScriptButton } from "./LaunchScriptButton";
import { LaunchScriptEntryButton } from "./LaunchScriptEntryButton";
import type { WorkspaceLaunchScriptsState } from "../hooks/useWorkspaceLaunchScripts";
import { useMenuController } from "../hooks/useMenuController";
import {
ThreadInfoPrompt,
type ThreadInfoPromptThread,
} from "../../threads/components/ThreadInfoPrompt";

type MainHeaderProps = {
workspace: WorkspaceInfo;
Expand All @@ -36,6 +41,9 @@ type MainHeaderProps = {
onCreateBranch: (name: string) => Promise<void> | void;
canCopyThread?: boolean;
onCopyThread?: () => void | Promise<void>;
activeThreadInfo?: ThreadInfoPromptThread | null;
onRenameActiveThreadName?: (name: string) => Promise<void> | void;
onGenerateActiveThreadName?: () => Promise<string | null>;
onToggleTerminal: () => void;
isTerminalOpen: boolean;
showTerminalButton?: boolean;
Expand Down Expand Up @@ -89,6 +97,9 @@ export function MainHeader({
onCreateBranch,
canCopyThread = false,
onCopyThread,
activeThreadInfo = null,
onRenameActiveThreadName,
onGenerateActiveThreadName,
onToggleTerminal,
isTerminalOpen,
showTerminalButton = true,
Expand All @@ -110,6 +121,7 @@ export function MainHeader({
const [branchQuery, setBranchQuery] = useState("");
const [error, setError] = useState<string | null>(null);
const [copyFeedback, setCopyFeedback] = useState(false);
const [threadInfoOpen, setThreadInfoOpen] = useState(false);
const copyTimeoutRef = useRef<number | null>(null);
const renameInputRef = useRef<HTMLInputElement | null>(null);
const renameConfirmRef = useRef<HTMLButtonElement | null>(null);
Expand Down Expand Up @@ -166,6 +178,12 @@ export function MainHeader({
};
}, []);

useEffect(() => {
if (!activeThreadInfo) {
setThreadInfoOpen(false);
}
}, [activeThreadInfo]);

const handleCopyClick = async () => {
if (!onCopyThread) {
return;
Expand Down Expand Up @@ -551,6 +569,21 @@ export function MainHeader({
<Terminal size={14} aria-hidden />
</button>
)}
<button
type="button"
className="ghost main-header-action"
onClick={() => {
setThreadInfoOpen(true);
}}
disabled={
!activeThreadInfo || !onRenameActiveThreadName || !onGenerateActiveThreadName
}
data-tauri-drag-region="false"
aria-label="Thread info"
title="Thread info"
>
<Info size={14} aria-hidden />
</button>
<button
type="button"
className={`ghost main-header-action${copyFeedback ? " is-copied" : ""}`}
Expand All @@ -567,6 +600,17 @@ export function MainHeader({
</button>
{extraActionsNode}
</div>
{threadInfoOpen &&
activeThreadInfo &&
onRenameActiveThreadName &&
onGenerateActiveThreadName ? (
Comment on lines +603 to +606

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset thread info modal state when thread context is lost

Because threadInfoOpen is never cleared when activeThreadInfo becomes falsy, the modal can silently unmount (for example when the user navigates to workspace home or the active thread is temporarily unset) while the open flag stays true; as soon as another thread becomes active, the modal reappears automatically without an explicit click. This creates an unexpected cross-thread modal reopen behavior that is user-visible and confusing.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bb5c1e3. Added a useEffect in MainHeader to reset threadInfoOpen whenever activeThreadInfo becomes falsy, so the modal cannot silently stay open and auto-reappear on a different thread context.

<ThreadInfoPrompt
thread={activeThreadInfo}
onClose={() => setThreadInfoOpen(false)}
onSaveName={onRenameActiveThreadName}
onGenerateName={onGenerateActiveThreadName}
/>
) : null}
</header>
);
}
3 changes: 3 additions & 0 deletions src/features/layout/hooks/layoutNodes/buildPrimaryNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ export function buildPrimaryNodes(options: LayoutNodesOptions): PrimaryLayoutNod
onCreateBranch={options.onCreateBranch}
canCopyThread={options.activeItems.length > 0}
onCopyThread={options.onCopyThread}
activeThreadInfo={options.activeThreadInfo}
onRenameActiveThreadName={options.onRenameActiveThreadName}
onGenerateActiveThreadName={options.onGenerateActiveThreadName}
onToggleTerminal={options.onToggleTerminal}
isTerminalOpen={options.terminalOpen}
showTerminalButton={options.showTerminalButton}
Expand Down
4 changes: 4 additions & 0 deletions src/features/layout/hooks/layoutNodes/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DragEvent, MouseEvent, ReactNode, RefObject } from "react";
import type { ReviewPromptState, ReviewPromptStep } from "../../../threads/hooks/useReviewPrompt";
import type { WorkspaceLaunchScriptsState } from "../../../app/hooks/useWorkspaceLaunchScripts";
import type { ThreadInfoPromptThread } from "../../../threads/components/ThreadInfoPrompt";
import type {
AccessMode,
ApprovalRequest,
Expand Down Expand Up @@ -229,6 +230,9 @@ export type LayoutNodesOptions = {
) => Promise<void> | void;
onCreateBranch: (name: string) => Promise<void>;
onCopyThread: () => void | Promise<void>;
activeThreadInfo?: ThreadInfoPromptThread | null;
onRenameActiveThreadName?: (name: string) => void | Promise<void>;
onGenerateActiveThreadName?: () => Promise<string | null>;
onToggleTerminal: () => void;
showTerminalButton: boolean;
showWorkspaceTools: boolean;
Expand Down
Loading
Loading