diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 4a9bdfeed6bb..686a1891a772 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -2165,8 +2165,6 @@ export default function ChatView(props: ChatViewProps) { timelineEntries, timelineAnchorMessageId, setTimelineAnchor, - onNavigateHistory: () => composerRef.current?.collapseForTimelineNavigation(), - onReachedEnd: () => composerRef.current?.restoreAfterTimelineReachedEnd(), }); // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- schedules a frame to focus the composer DOM element when a thread opens with the terminal closed diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index 733b03717c9f..5c321cb46aef 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -9,7 +9,6 @@ import { resolveDefaultBranchActionDialogCopy, resolveLiveThreadBranchUpdate, resolveQuickAction, - buildGitActionProgressStages, resolveThreadBranchUpdate, } from "./GitActionsControl.logic"; @@ -475,88 +474,6 @@ describe("resolveDefaultBranchActionDialogCopy", () => { }); }); -describe("buildGitActionProgressStages", () => { - it("shows only push progress for explicit push actions", () => { - const stages = buildGitActionProgressStages({ - action: "push", - hasCustomCommitMessage: false, - hasWorkingTreeChanges: false, - pushTarget: "origin/feature/test", - }); - assert.deepEqual(stages, ["Pushing to origin/feature/test..."]); - }); - - it("shows push and PR progress for create-pr actions that still need a push", () => { - const stages = buildGitActionProgressStages({ - action: "create_pr", - hasCustomCommitMessage: false, - hasWorkingTreeChanges: false, - pushTarget: "origin/feature/test", - shouldPushBeforePr: true, - }); - assert.deepEqual(stages, [ - "Pushing to origin/feature/test...", - "Preparing PR...", - "Generating PR content...", - "Creating pull request...", - ]); - }); - - it("shows only PR progress when create-pr can skip the push", () => { - const stages = buildGitActionProgressStages({ - action: "create_pr", - hasCustomCommitMessage: false, - hasWorkingTreeChanges: false, - shouldPushBeforePr: false, - }); - assert.deepEqual(stages, [ - "Preparing PR...", - "Generating PR content...", - "Creating pull request...", - ]); - }); - - it("includes commit stages for commit+push when working tree is dirty", () => { - const stages = buildGitActionProgressStages({ - action: "commit_push", - hasCustomCommitMessage: false, - hasWorkingTreeChanges: true, - pushTarget: "origin/feature/test", - }); - assert.deepEqual(stages, [ - "Generating commit message...", - "Committing...", - "Pushing to origin/feature/test...", - ]); - }); - - it("includes granular PR stages for commit+push+PR actions", () => { - const stages = buildGitActionProgressStages({ - action: "commit_push_pr", - hasCustomCommitMessage: true, - hasWorkingTreeChanges: true, - pushTarget: "origin/feature/test", - }); - assert.deepEqual(stages, [ - "Committing...", - "Pushing to origin/feature/test...", - "Preparing PR...", - "Generating PR content...", - "Creating pull request...", - ]); - }); - - it("names the feature ref with Git nouns when no VCS terminology is supplied", () => { - const stages = buildGitActionProgressStages({ - action: "commit", - hasCustomCommitMessage: true, - hasWorkingTreeChanges: true, - featureBranch: true, - }); - assert.deepEqual(stages, ["Preparing feature branch...", "Committing..."]); - }); -}); - describe("resolveThreadBranchUpdate", () => { it("returns a branch update when the action created a new branch", () => { const update = resolveThreadBranchUpdate({ diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index 22b493295262..e4fcdf9bfde1 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -111,48 +111,6 @@ export function formatGitActionElapsed(startedAtMs: number | null, nowMs: number return `${minutes}m ${seconds}s`; } -export function buildGitActionProgressStages(input: { - action: GitStackedAction; - hasCustomCommitMessage: boolean; - hasWorkingTreeChanges: boolean; - pushTarget?: string; - featureBranch?: boolean; - shouldPushBeforePr?: boolean; - terminology?: ChangeRequestTerminology; - vcsTerminology?: VcsTerminology; -}): string[] { - const terminology = input.terminology ?? sourceControlClients.get(undefined).changeRequest; - const vcs = input.vcsTerminology ?? DEFAULT_VCS_TERMINOLOGY; - const branchStages = input.featureBranch ? [`Preparing feature ${vcs.refNoun}...`] : []; - const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing..."; - const prStages = [ - `Preparing ${terminology.shortLabel}...`, - `Generating ${terminology.shortLabel} content...`, - `Creating ${terminology.singular}...`, - ]; - - if (input.action === "push") { - return [pushStage]; - } - if (input.action === "create_pr") { - return input.shouldPushBeforePr ? [pushStage, ...prStages] : prStages; - } - - const shouldIncludeCommitStages = input.action === "commit" || input.hasWorkingTreeChanges; - const commitStages = !shouldIncludeCommitStages - ? [] - : input.hasCustomCommitMessage - ? ["Committing..."] - : ["Generating commit message...", "Committing..."]; - if (input.action === "commit") { - return [...branchStages, ...commitStages]; - } - if (input.action === "commit_push") { - return [...branchStages, ...commitStages, pushStage]; - } - return [...branchStages, ...commitStages, pushStage, ...prStages]; -} - export function buildMenuItems( gitStatus: VcsStatusResult | null, isBusy: boolean, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 98f6b002b389..5dcd44dc0803 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -295,8 +295,6 @@ function consumePasteAsTextShortcut(deadline: { current: number }): boolean { } export interface ChatComposerHandle { - collapseForTimelineNavigation: () => void; - restoreAfterTimelineReachedEnd: () => void; focusAtEnd: () => void; focusAt: (cursor: number) => void; addDroppedFiles: (files: File[]) => void; @@ -1053,7 +1051,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const isMobileViewport = useMediaQuery("max-sm"); const isSoftwareKeyboardOpen = useSoftwareKeyboardOpen(); const [isComposerFocused, setIsComposerFocused] = useState(false); - const [scrollCollapsedTarget, setScrollCollapsedTarget] = useState(null); const [composerSubmissionError, setComposerSubmissionError] = useState(null); const [providerInputSubmissionError, setProviderInputSubmissionError] = useState( null, @@ -1069,21 +1066,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) !isComposerFocused && !hasMultilinePrompt && pendingUserInputs.length === 0; - const isComposerScrollCollapsed = - !isMobileViewport && - routeKind === "server" && - settings.composerCollapseOnScroll && - scrollCollapsedTarget === composerDraftTargetKey && - !hasMultilinePrompt && - !isEditingQueuedMessage && - composerTrigger === null && - pendingUserInputs.length === 0 && - activePendingApproval === null && - !isDragOverComposer && - !isPreparingWorktree && - composerSubmissionError === null && - providerInputSubmissionError === null; - const isComposerCollapsed = isComposerCollapsedMobile || isComposerScrollCollapsed; const composerEditorRef = useRef(null); const pasteAsTextShortcutUntilRef = useRef(0); @@ -1236,7 +1218,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const showComposerTopDrawer = pendingUserInputs.length > 0 || (!isComposerCollapsedMobile && showPlanFollowUpPrompt && activeProposedPlan !== null); - const showCollapsedMobilePromptRow = isComposerCollapsed; + const showCollapsedMobilePromptRow = isComposerCollapsedMobile; const showComposerAttachAction = fileStagingLimit !== null && (!activePendingProgress || @@ -1636,7 +1618,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) cursorAdjacentToMention: boolean, contextIds: string[], ) => { - setScrollCollapsedTarget(null); refreshWorkspaceSnapshotOnTyping(); if (activePendingProgress?.activeQuestion && pendingUserInputs.length > 0) { if (activePendingProgress.activeQuestion.allowCustomAnswer === false) return; @@ -2365,7 +2346,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }; const compactThreadContextStable = useStableCallback(compactThreadContext); const expandMobileComposer = () => { - setScrollCollapsedTarget(null); if (composerBlurFrameRef.current !== null) { window.cancelAnimationFrame(composerBlurFrameRef.current); composerBlurFrameRef.current = null; @@ -2466,8 +2446,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) expandMobileComposer: expandMobileComposerStable, }); const hasBlockingComposerTopDrawer = pendingUserInputs.length > 0; - const composerControlsCollapsed = isComposerCollapsed; - const showInlineRestingControls = isComposerCollapsed; + const composerControlsCollapsed = isComposerCollapsedMobile; + // The collapsed mobile web composer keeps a second controls row inside the card. + const showInlineRestingControls = isComposerCollapsedMobile; const composerControlsHidden = showInlineRestingControls && !restingControlsVisible; if (composerControlsHidden && isComposerModelPickerOpen) { setIsComposerModelPickerOpen(false); @@ -2704,11 +2685,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }; const insertComposerTextAtEndStable = useStableCallback(insertComposerTextAtEnd); - // The scroll-collapsed editor is hidden, so it has to render before it can take focus. - const revealScrollCollapsedEditor = () => { - if (isComposerScrollCollapsed) flushSync(() => setScrollCollapsedTarget(null)); - }; - const typeComposerKey: ChatComposerHandle["typeKey"] = (event) => { if (!composerVimActive) return insertComposerTextAtEnd(event.key); // Vim runs keys as commands at its caret, which only its focused editor can do. A @@ -2717,7 +2693,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) expandMobileComposer(); return true; } - revealScrollCollapsedEditor(); composerEditorRef.current?.focus(); return ( composerEditorRef.current?.handleVimKey(event) || insertComposerText(event.key, "cursor") @@ -3023,7 +2998,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }, []); const openModelPicker = () => { - setScrollCollapsedTarget(null); if (composerControlsHidden) { if (composerBlurFrameRef.current !== null) { window.cancelAnimationFrame(composerBlurFrameRef.current); @@ -3039,15 +3013,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) composerRef, () => ({ focusAtEnd: () => { - revealScrollCollapsedEditor(); composerEditorRef.current?.focusAtEnd(); }, focusAt: (cursor: number) => { - revealScrollCollapsedEditor(); composerEditorRef.current?.focusAt(cursor); }, - collapseForTimelineNavigation: () => setScrollCollapsedTarget(composerDraftTargetKey), - restoreAfterTimelineReachedEnd: () => setScrollCollapsedTarget(null), addDroppedFiles: (files: File[]) => { void addComposerAttachments(files).then((inserted) => { if (!inserted) focusComposer(); @@ -3375,7 +3345,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) composerBlurFrameRef.current = null; } setIsComposerFocused(true); - setScrollCollapsedTarget(null); }} onBlurCapture={(event) => { if ( @@ -3526,7 +3495,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) className={cn( "relative px-4 pb-2", "pt-3.5 sm:pt-4", - isComposerCollapsed && "hidden", + isComposerCollapsedMobile && "hidden", )} > {isStashMenuOpen && !composerMenuOpen && ( @@ -3707,7 +3676,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ) : null} - {isComposerCollapsed && speechInput.state.phase === "idle" ? null : ( + {isComposerCollapsedMobile && speechInput.state.phase === "idle" ? null : (
; readonly timelineAnchorMessageId: MessageId | null; readonly setTimelineAnchor: Dispatch>; - readonly onNavigateHistory: () => void; - readonly onReachedEnd: () => void; } /** @@ -308,7 +306,6 @@ export function useTimelineFollow(input: TimelineFollowInput) { ); const handlePageScrollStart = useStableCallback((key: PageScrollKey) => { if ((key === "PageUp" && timelineRealContentOverflowsViewport()) || !isTimelineAtLogicalEnd()) { - input.onNavigateHistory(); // Through the viewport, so a running position restore stops too. viewportRef.current?.navigate({ releasesFollow: true }); } @@ -326,7 +323,6 @@ export function useTimelineFollow(input: TimelineFollowInput) { }; const onComposerPageScrollReleaseStable = useStableCallback(onComposerPageScrollRelease); const scrollToEnd = (animated: boolean = false) => { - input.onReachedEnd(); // Stops a running position restore; follow itself is re-armed below. viewportRef.current?.navigate({ releasesFollow: false }); followStore.dispatch(visit, { type: "followEnd" }); @@ -368,10 +364,7 @@ export function useTimelineFollow(input: TimelineFollowInput) { contentScrollsUp: timelineRealContentOverflowsViewport, awayFromEnd, }); - if (releases) { - releaseFollowForNavigation(); - input.onNavigateHistory(); - } + if (releases) releaseFollowForNavigation(); }; const handleNavigationStable = useStableCallback(handleNavigation); /** @@ -495,7 +488,6 @@ export function useTimelineFollow(input: TimelineFollowInput) { const onTimelineFooterGrowStable = useStableCallback(onTimelineFooterGrow); const onIsAtEndChange = (isAtEnd: boolean) => { - if (isAtEnd) input.onReachedEnd(); const scroll = visitScroll(); if (!isAtEnd && readFollow().mode === "following") { scroll.showScrollDebouncer.cancel(); diff --git a/apps/web/src/components/guest/GuestPage.tsx b/apps/web/src/components/guest/GuestPage.tsx index 74d8e0ae1a26..7e41e9a8ca29 100644 --- a/apps/web/src/components/guest/GuestPage.tsx +++ b/apps/web/src/components/guest/GuestPage.tsx @@ -465,8 +465,6 @@ function GuestThread({ timelineEntries: entries, timelineAnchorMessageId: null, setTimelineAnchor: ignore, - onNavigateHistory: ignore, - onReachedEnd: ignore, }); const renderHtmlRender = useStableCallback( (row: Extract) => ( diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 4b1eea94209f..14fa18d74d59 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -1108,12 +1108,6 @@ export function ChatSettingsPanel() { description: "Use the system clipboard for Vim yanks, deletes, and pastes. When off, use a private Vim register.", }, - { - key: "composerCollapseOnScroll", - searchId: "composer-collapse", - description: - "Rest the composer of an existing thread into a single line when you scroll the conversation. Focus the composer or start typing to expand it again.", - }, ] as const; return ( diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 9634538b39ba..b206e967c897 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -725,12 +725,6 @@ export const SETTINGS_SEARCH_ITEMS = [ to: "/settings/chat", searchTerms: ["nvim neovim vim yank paste register copy"], }, - { - id: "composer-collapse", - title: "Collapse composer on scroll", - to: "/settings/chat", - searchTerms: ["composer rest resting scroll wheel conversation timeline shrink minimize"], - }, { id: "send-shortcut", title: "Send shortcut", diff --git a/apps/web/src/projectIconOptions.ts b/apps/web/src/projectIconOptions.ts index 97deef3c0b10..69e9120046bb 100644 --- a/apps/web/src/projectIconOptions.ts +++ b/apps/web/src/projectIconOptions.ts @@ -1,8 +1,8 @@ -import { iconNames, type IconName } from "lucide-react/dynamic"; import { searchItems } from "@t3tools/shared/searchRanking"; +import { lucideIconNames } from "./lib/lucideIconByName"; export { PROJECT_ICON_COLORS, projectIconColorClassName } from "./projectIconColors"; -const POPULAR_PROJECT_ICONS: ReadonlyArray = [ +const POPULAR_PROJECT_ICONS: ReadonlyArray = [ "folder-code", "code-2", "terminal", @@ -62,10 +62,10 @@ export const PROJECT_EMOJIS: ReadonlyArray<{ readonly emoji: string; readonly la { emoji: "🌱", label: "Seedling" }, ]; -export function filterProjectIconNames(query: string): ReadonlyArray { +export function filterProjectIconNames(query: string): ReadonlyArray { const normalized = query.trim(); if (!normalized) return POPULAR_PROJECT_ICONS; - return searchItems(iconNames, normalized, (name) => [name], 60); + return searchItems(lucideIconNames, normalized, (name) => [name], 60); } export function firstEmoji(value: string): string | null { diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index f29728f9e581..7601f6752452 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -504,7 +504,6 @@ export const ClientSettingsSchema = Schema.Struct({ planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), contextWindowMeterEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), composerRichTextEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), - composerCollapseOnScroll: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), composerVimEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), composerVimSystemClipboard: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(false)), @@ -1948,7 +1947,6 @@ export const ClientSettingsPatch = Schema.Struct({ planModeEnabled: Schema.optionalKey(Schema.Boolean), contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean), composerRichTextEnabled: Schema.optionalKey(Schema.Boolean), - composerCollapseOnScroll: Schema.optionalKey(Schema.Boolean), composerVimEnabled: Schema.optionalKey(Schema.Boolean), composerVimSystemClipboard: Schema.optionalKey(Schema.Boolean), sendShortcut: Schema.optionalKey(Schema.Literals(["enter", "mod-enter-multiline", "mod-enter"])), diff --git a/wiki/log.md b/wiki/log.md index 33c02f670210..0782cc70463f 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -2,6 +2,8 @@ ## 2026-11-07 +- **Correction**: Composer scroll-collapse was upstream's, not fork behavior. Upstream removed it, so the fork drops it too; [upstream integration](upstream-integration.md#completed-november-2-sync) no longer lists it as retained. + - **Compatibility**: Recorded the fork-only [Tailscale IPv4 check for linked environments](upstream-integration.md#tailscale-ipv4-addresses-for-linked-environments). A server may now link to a machine at a Tailscale IPv4 address that its own Tailscale lists. - **Remote deployment target**: Nicholas's remote server is now `nicholas-dev`, a `t3 up` boot service. [Upstream integration](upstream-integration.md#ssh-runtime-release-handoff) notes that the SSH deployment helper does not apply there and points to the service procedure in `AGENTS.md`. diff --git a/wiki/upstream-integration.md b/wiki/upstream-integration.md index cd1ac7c5fd53..f237563fb4bc 100644 --- a/wiki/upstream-integration.md +++ b/wiki/upstream-integration.md @@ -6,7 +6,7 @@ tags: [upstream, fork, integration] status: stable generated: by: claude-code/claude-opus-5-5 - at: 2026-11-07T18:46:00+00:00 + at: 2026-11-07T18:53:28+00:00 verified: - by: claude-code/claude-opus-5-5 at: 2026-11-07T17:09:56+00:00 @@ -87,6 +87,9 @@ sources: - id: opencode-child-tracking resource: ../packages/provider-opencode/src/server/v2/adapter.ts title: Upstream OpenCode lifecycle with fork reused-child and input-delivery safeguards + - id: composer-collapse-removal + resource: https://github.com/pingdotgg/t3code/commit/bd84b1781a7febb7398a6838cad0416a3240da1a + title: Upstream removes the collapsed composer and its scroll-collapse setting - id: october-16-main-integration resource: https://github.com/NicholasZolton/t3code/pull/50 title: Completed October 16 upstream integration into fork main @@ -428,14 +431,17 @@ restore the old thread scorer. Git and Jujutsu share the upstream-compatible change-request workflow rather than separate GitHub-specific helpers. [^november-2-local-integration][^shared-pr-workflow] -Unreplaced fork behavior remains: saved prompts, Vim, composer scroll-collapse, -OpenCode account controls and child-input tracking, notification privacy, -Jujutsu, SSH readiness safeguards, Crit/Portless, persistent Tailscale publication, -and private signing. Composer collapse follows upstream timeline-navigation signals; -missing-workspace recovery stays at the shared workflow boundary and respects the +Unreplaced fork behavior remains: saved prompts, Vim, OpenCode account controls +and child-input tracking, notification privacy, Jujutsu, SSH readiness safeguards, +Crit/Portless, persistent Tailscale publication, and private signing. +Missing-workspace recovery stays at the shared workflow boundary and respects the recorded backend owner instead of silently switching Git/Jujutsu ownership. [^november-2-local-integration][^workspace-recovery][^opencode-child-tracking] +Composer scroll-collapse is upstream's, not the fork's. Upstream removed it with +its setting, so syncs keep it removed; this merge had kept it as fork behavior. +[^composer-collapse-removal] + ## Completed October 16 sync [PR #50](https://github.com/NicholasZolton/t3code/pull/50) merged on October 16, @@ -1006,6 +1012,8 @@ git log --reverse --oneline origin/main..upstream/main [^opencode-child-tracking]: Native OpenCode lifecycle with reused-child admission, execution attribution, and delivery tracking. +[^composer-collapse-removal]: Upstream #25869 deletes the collapsed composer state and the `composerCollapseOnScroll` setting. + [^november-4-local-integration]: Upstream merge and follow-up API adaptations, now incorporated into main through PR #55. [^november-4-review-git]: Exact revisions, ancestry, separate main and branch gaps, and upstream-first source comparison.