Skip to content

Commit af5ac29

Browse files
fix: reconcile main's round-8 features after the rebase
Re-applies the deltas that mid-stack blob reverts discarded, and merges main's work into the v2-owned surfaces: - keybindings: main's STATIC_KEYBINDING_COMMANDS rename plus both new commands (rightPanel.toggleMaximized alongside threadPanel.toggle) - OpenInPicker: main's remote-open/SSH routing and favorite-editor shortcut layered onto the branch's panel/toolbar variants; the extracted shouldShowOpenInPicker now takes remoteOpenMode - ChatMarkdown: main's bare-filename resolver (#6297) ported into the branch's module-level component factory, plus #4133 title-attribute stripping on links and images - ComposerPrimaryActions: main's #4781 model (stop stays reachable, send joins it when Enter-to-send is unavailable) carrying the branch's steering send button - ComposerPendingUserInputPanel: main's collapsible redesign with the v2 RuntimeRequestId and responseCapability gate - ChatComposer: main's oversized-prompt submission guard wrapping the branch's dispatch-mode send - preview shell: main's container-aware width clamp ported into the branch's usePreviewPanelInlineSize hook - MessagesTimeline/Sidebar: main's day-aware timestamps, code-font tool bodies and provider accent badges on the v2 runtime shell - index.css: main's @variant dark migration (#6381) replaces the branch's standalone .dark block - contracts: main's send-turn image mime allowlist re-homed to chatAttachment.ts, where v2 keeps the other send-turn limits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2ae0e11 commit af5ac29

31 files changed

Lines changed: 1361 additions & 1202 deletions

‎apps/mobile/src/features/threads/NewTaskDraftScreen.tsx‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ import { enqueueThreadOutboxMessage } from "../../state/thread-outbox";
8787
import { removeThreadOutboxMessage } from "../../state/thread-outbox-removal";
8888
import { useRemoteConnectionStatus } from "../../state/use-remote-environment-registry";
8989
import { useNewTaskFlow } from "./new-task-flow-provider";
90+
import { resolveProjectThreadCreationBranch } from "./projectThreadCreationValidation";
9091
import { useCreateProjectThread } from "./use-project-actions";
9192
import { resolveDraftProjectSelection } from "./new-task-project-selection";
9293
import {
@@ -757,11 +758,17 @@ export function NewTaskDraftScreen(props: {
757758
flow.environments.find(
758759
(environment) => environment.environmentId === flow.selectedEnvironmentId,
759760
)?.environmentLabel ?? "Environment";
760-
const currentBranchName =
761+
const availableCurrentBranchName =
761762
flow.availableBranches.find((branch) => branch.current)?.name ??
762763
flow.availableBranches.find((branch) => branch.isDefault)?.name ??
763764
null;
764-
const selectedBranchName = flow.selectedBranchName ?? currentBranchName;
765+
const selectedBranchName = resolveProjectThreadCreationBranch({
766+
workspaceMode: flow.workspaceMode,
767+
selectedBranch:
768+
flow.selectedBranchName ??
769+
(flow.workspaceMode === "worktree" ? availableCurrentBranchName : null),
770+
currentCheckoutBranch: flow.currentCheckoutBranchName,
771+
});
765772
const selectedBranchLabel = resolveNewTaskBranchLabel({
766773
branchName: selectedBranchName,
767774
startFromOrigin: flow.startFromOrigin,
@@ -947,11 +954,16 @@ export function NewTaskDraftScreen(props: {
947954
}),
948955
projectTitle: selectedProject.title,
949956
});
957+
const creationBranch = resolveProjectThreadCreationBranch({
958+
workspaceMode,
959+
selectedBranch: selectedBranchName,
960+
currentCheckoutBranch: flow.currentCheckoutBranchName,
961+
});
950962
const result = await createProjectThread({
951963
project: selectedProject,
952964
modelSelection,
953965
envMode: workspaceMode,
954-
branch: selectedBranchName,
966+
branch: creationBranch,
955967
worktreePath: workspaceMode === "worktree" ? null : selectedWorktreePath,
956968
startFromOrigin,
957969
runtimeMode,

‎apps/mobile/src/features/threads/ThreadComposer.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
332332
const showStopAction = props.canStopThread;
333333

334334
const sendLabel =
335-
props.connectionState !== "connected" || props.activeThreadBusy || props.queueCount > 0
336-
? "Queue"
337-
: "Send";
335+
props.connectionState !== "connected" || props.queueCount > 0 ? "Queue" : "Send";
338336
const currentModelSelection = props.selectedThread.modelSelection;
339337
const currentRuntimeMode = props.selectedThread.runtimeMode;
340338
const connectionStatus = composerConnectionStatus({

‎apps/server/src/ws.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ import * as ReviewService from "./review/ReviewService.ts";
128128
import * as ProjectEnrichmentService from "./project/ProjectEnrichmentService.ts";
129129
import * as ProjectService from "./project/ProjectService.ts";
130130
import * as ServerEnvironment from "./environment/ServerEnvironment.ts";
131+
import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts";
131132
import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";
132133
import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
133134
import { requiredScopeForRpcMethod } from "./auth/RpcAuthorization.ts";
@@ -437,6 +438,7 @@ const makeWsRpcLayer = (
437438
const checkpointDiffQuery = yield* CheckpointDiffQuery.CheckpointDiffQuery;
438439
const keybindings = yield* Keybindings.Keybindings;
439440
const externalLauncher = yield* ExternalLauncher.ExternalLauncher;
441+
const remoteOpenTargets = yield* RemoteOpenTargets.RemoteOpenTargets;
440442
const gitWorkflow = yield* GitWorkflowService.GitWorkflowService;
441443
const review = yield* ReviewService.ReviewService;
442444
const vcsProvisioning = yield* VcsProvisioningService.VcsProvisioningService;
@@ -576,6 +578,11 @@ const makeWsRpcLayer = (
576578
availableEditors: yield* resolveAvailableEditorsForConfig(
577579
externalLauncher.resolveAvailableEditors(),
578580
),
581+
// Same discovery-with-timeout treatment as editors: a slow probe
582+
// must not stall server.getConfig, so it degrades to no targets.
583+
remoteOpenTargets: yield* resolveAvailableEditorsForConfig(
584+
remoteOpenTargets.resolveTargets(),
585+
),
579586
observability: {
580587
logsDirectoryPath: config.logsDir,
581588
localTracingEnabled: true,

‎apps/web/src/components/BranchToolbar.logic.test.ts‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
resolveLocalCheckoutBranchMismatch,
1818
resolvePreviousWorktreeLabel,
1919
resolvePreviousWorktreeSeed,
20+
sanitizeNewRefName,
2021
shouldIncludeBranchPickerItem,
2122
shouldShowComposerContextStrip,
2223
shouldShowEnvironmentIndicator,
@@ -756,4 +757,93 @@ describe("shouldIncludeBranchPickerItem", () => {
756757
}),
757758
).toBe(false);
758759
});
760+
761+
// Typing a spaced name must still surface the ref it would have been created
762+
// as, or the picker shows nothing at all for that query.
763+
it("surfaces an existing ref matching the sanitized query", () => {
764+
expect(
765+
shouldIncludeBranchPickerItem({
766+
itemValue: "new-branch",
767+
normalizedQuery: "new branch",
768+
createBranchItemValue: null,
769+
checkoutPullRequestItemValue: null,
770+
}),
771+
).toBe(true);
772+
});
773+
774+
// A partial query has to reach the ref it would have been created as, so
775+
// searching "hello w" still finds an existing hello-world.
776+
it("surfaces a ref from a partial query containing a space", () => {
777+
expect(
778+
shouldIncludeBranchPickerItem({
779+
itemValue: "hello-world",
780+
normalizedQuery: "hello w",
781+
createBranchItemValue: null,
782+
checkoutPullRequestItemValue: null,
783+
}),
784+
).toBe(true);
785+
});
786+
787+
it("excludes refs matching neither the raw nor the sanitized query", () => {
788+
expect(
789+
shouldIncludeBranchPickerItem({
790+
itemValue: "main",
791+
normalizedQuery: "new branch",
792+
createBranchItemValue: null,
793+
checkoutPullRequestItemValue: null,
794+
}),
795+
).toBe(false);
796+
});
797+
});
798+
799+
// Git rejects ASCII space and the ASCII control characters in ref names, so a
800+
// typed name like "new branch" can only ever fail. Replacing exactly those can
801+
// turn a failing name into a working one without touching a name git already
802+
// accepts, including one holding non-ASCII whitespace such as U+00A0.
803+
describe("sanitizeNewRefName", () => {
804+
it("replaces a space with a dash", () => {
805+
expect(sanitizeNewRefName("new branch")).toBe("new-branch");
806+
});
807+
808+
it("collapses a run of whitespace into a single dash", () => {
809+
expect(sanitizeNewRefName("new branch")).toBe("new-branch");
810+
});
811+
812+
it("trims surrounding whitespace instead of turning it into dashes", () => {
813+
expect(sanitizeNewRefName(" new branch ")).toBe("new-branch");
814+
});
815+
816+
it("replaces tabs, which git rejects just like spaces", () => {
817+
expect(sanitizeNewRefName("new\tbranch")).toBe("new-branch");
818+
});
819+
820+
// git accepts U+00A0, U+2009 and other non-ASCII whitespace in ref names, so
821+
// rewriting them would silently create a ref the user never typed.
822+
it("preserves whitespace that git accepts", () => {
823+
expect(sanitizeNewRefName("new\u00a0branch")).toBe("new\u00a0branch");
824+
expect(sanitizeNewRefName("new\u2009branch")).toBe("new\u2009branch");
825+
});
826+
827+
it("keeps slashes so nested ref names survive", () => {
828+
expect(sanitizeNewRefName("feature/new thing")).toBe("feature/new-thing");
829+
});
830+
831+
it("preserves case because git ref names are case sensitive", () => {
832+
expect(sanitizeNewRefName("Feature/New Thing")).toBe("Feature/New-Thing");
833+
});
834+
835+
it("leaves an already valid ref name untouched", () => {
836+
expect(sanitizeNewRefName("feature/login")).toBe("feature/login");
837+
});
838+
839+
it("returns an empty string for whitespace-only input", () => {
840+
expect(sanitizeNewRefName(" ")).toBe("");
841+
});
842+
843+
// Scoped deliberately to whitespace: git accepts consecutive dashes, so
844+
// collapsing them would rewrite names the user may have typed on purpose.
845+
it("does not collapse dashes the user typed", () => {
846+
expect(sanitizeNewRefName("new - branch")).toBe("new---branch");
847+
expect(sanitizeNewRefName("foo--bar")).toBe("foo--bar");
848+
});
759849
});

‎apps/web/src/components/ChatView.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
265265
import { MessagesTimeline, type MessagesTimelineHistoryControls } from "./chat/MessagesTimeline";
266266
import { resolveTimelineIsAtEnd } from "./chat/MessagesTimeline.logic";
267267
import { ChatHeader } from "./chat/ChatHeader";
268+
import { useRemoteOpenState } from "~/remoteOpen";
268269
import { shouldShowOpenInPicker } from "./chat/OpenInPicker.logic";
269270
import { useOpenFavoriteEditorShortcut } from "./chat/OpenInPickerShortcut";
270271
import {
@@ -2744,10 +2745,12 @@ function ChatViewContent(props: ChatViewProps) {
27442745
});
27452746
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
27462747
const availableEditors = useAtomValue(primaryServerAvailableEditorsAtom);
2748+
const remoteOpenState = useRemoteOpenState(activeThread?.environmentId ?? environmentId);
27472749
const showOpenInPicker = shouldShowOpenInPicker({
27482750
activeProjectName: activeProject?.title,
27492751
activeThreadEnvironmentId: activeThread?.environmentId ?? environmentId,
27502752
primaryEnvironmentId,
2753+
remoteOpenMode: remoteOpenState.mode,
27512754
});
27522755
useOpenFavoriteEditorShortcut({
27532756
enabled: showOpenInPicker,

0 commit comments

Comments
 (0)