merge: integrate main into dev

Resolve 9 merge conflicts preserving both sides' intent:
- nix hash, bootstrap hostnames rename, websocket-server wiring
- session.ts handler dedup, snapshot fallback, worktree registration
- worktree-session git service + script/setup features
- sidebar-workspace-list setup navigation, session-context cleanup
- workspace-registry-bootstrap test kept (code still exists)
- checkout-git test additions from both sides

Post-merge fixes: isPaneFocused prop rename, server type rebuild.
This commit is contained in:
Mohamed Boudra
2026-04-15 16:07:39 +07:00
73 changed files with 2288 additions and 1126 deletions
@@ -43,12 +43,17 @@ export function BranchSwitcher({
queryClient,
});
if (!currentBranchName) {
return (
const titleContent = (
<>
<GitBranch size={14} color={theme.colors.foregroundMuted} />
<Text testID="workspace-header-title" style={styles.headerTitle} numberOfLines={1}>
{title}
</Text>
);
</>
);
if (!currentBranchName) {
return <View style={styles.branchSwitcherTrigger}>{titleContent}</View>;
}
return (
@@ -63,10 +68,7 @@ export function BranchSwitcher({
accessibilityRole="button"
accessibilityLabel={`Current branch: ${currentBranchName}. Press to switch branch.`}
>
<GitBranch size={14} color={theme.colors.foregroundMuted} />
<Text testID="workspace-header-title" style={styles.headerTitle} numberOfLines={1}>
{title}
</Text>
{titleContent}
{!isCompact ? <ChevronDown size={12} color={theme.colors.foregroundMuted} /> : null}
</Pressable>
<Combobox
@@ -117,7 +119,10 @@ const styles = StyleSheet.create((theme) => ({
xs: -theme.spacing[2],
md: 0,
},
paddingVertical: theme.spacing[1],
paddingVertical: {
xs: 0,
md: theme.spacing[1],
},
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.md,
flexShrink: 1,
+7 -7
View File
@@ -63,7 +63,7 @@ type ImageListUpdater = ImageAttachment[] | ((prev: ImageAttachment[]) => ImageA
interface ComposerProps {
agentId: string;
serverId: string;
isInputActive: boolean;
isPaneFocused: boolean;
onSubmitMessage?: (payload: MessagePayload) => Promise<void>;
/** When true, the submit button is enabled even without text or images (e.g. external attachment selected). */
hasExternalContent?: boolean;
@@ -106,7 +106,7 @@ const MOBILE_MESSAGE_PLACEHOLDER = "Message, @files, /commands";
export function Composer({
agentId,
serverId,
isInputActive,
isPaneFocused,
onSubmitMessage,
hasExternalContent = false,
allowEmptySubmit = false,
@@ -435,7 +435,7 @@ export function Composer({
const handleKeyboardAction = useCallback(
(action: KeyboardActionDefinition): boolean => {
if (!isInputActive) {
if (!isPaneFocused) {
return false;
}
@@ -479,7 +479,7 @@ export function Composer({
return false;
}
},
[isInputActive],
[isPaneFocused],
);
useKeyboardActionHandler({
@@ -493,9 +493,9 @@ export function Composer({
"message-input.voice-toggle",
"message-input.voice-mute-toggle",
],
enabled: isInputActive,
enabled: isPaneFocused,
priority: isMessageInputFocused ? 200 : 100,
isActive: () => isInputActive,
isActive: () => isPaneFocused,
handle: handleKeyboardAction,
});
@@ -766,7 +766,7 @@ export function Composer({
autoFocus={autoFocus && isDesktopWebBreakpoint}
autoFocusKey={`${serverId}:${agentId}`}
disabled={isSubmitLoading}
isInputActive={isInputActive}
isPaneFocused={isPaneFocused}
leftContent={leftContent}
beforeVoiceContent={beforeVoiceContent}
rightContent={rightContent}
@@ -264,6 +264,7 @@ export function FilePane({
return { file: payload.file ?? null, error: payload.error ?? null };
},
staleTime: 5_000,
refetchOnMount: true,
});
return (
+23 -4
View File
@@ -42,6 +42,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { Shortcut } from "@/components/ui/shortcut";
import { useWebElementScrollbar } from "@/components/use-web-scrollbar";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import { formatShortcut } from "@/utils/format-shortcut";
import { getShortcutOs } from "@/utils/shortcut-platform";
import type { MessageInputKeyboardActionKind } from "@/keyboard/actions";
import {
markScrollInvestigationEvent,
@@ -81,8 +83,8 @@ export interface MessageInputProps {
autoFocus?: boolean;
autoFocusKey?: string;
disabled?: boolean;
/** True when this input is the active composer. Used to gate global hotkeys and stop dictation when hidden. */
isInputActive?: boolean;
/** True when this composer's pane is focused. Used to gate global hotkeys and stop dictation when hidden. */
isPaneFocused?: boolean;
/** Content to render on the left side of the button row (e.g., AgentStatusBar) */
leftContent?: React.ReactNode;
/** Content to render on the right side before the voice button (e.g., context window meter) */
@@ -219,7 +221,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
autoFocus = false,
autoFocusKey,
disabled = false,
isInputActive = true,
isPaneFocused = true,
leftContent,
beforeVoiceContent,
rightContent,
@@ -247,7 +249,9 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
const voiceMuteToggleKeys = useShortcutKeys("voice-mute-toggle");
const dictationToggleKeys = useShortcutKeys("dictation-toggle");
const queueKeys = useShortcutKeys("message-input-queue");
const focusInputKeys = useShortcutKeys("focus-message-input");
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
const [isInputFocused, setIsInputFocused] = useState(false);
const rootRef = useRef<View | null>(null);
const inputWrapperRef = useRef<View | null>(null);
const textInputRef = useRef<TextInput | (TextInput & { getNativeRef?: () => unknown }) | null>(
@@ -441,7 +445,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
onError: handleDictationError,
canStart: canStartDictation,
canConfirm: canConfirmDictation,
autoStopWhenHidden: { isVisible: isInputActive },
autoStopWhenHidden: { isVisible: isPaneFocused },
enableDuration: true,
});
@@ -1025,10 +1029,12 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
accessibilityLabel="Message agent..."
onFocus={() => {
isInputFocusedRef.current = true;
setIsInputFocused(true);
onFocusChange?.(true);
}}
onBlur={() => {
isInputFocusedRef.current = false;
setIsInputFocused(false);
onFocusChange?.(false);
}}
style={[
@@ -1053,6 +1059,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
autoFocus={isWeb && autoFocus}
/>
{inputScrollbar}
{isWeb && isPaneFocused && !isInputFocused && !value && focusInputKeys ? (
<Text style={styles.focusHintText} pointerEvents="none">
{formatShortcut(focusInputKeys[0], getShortcutOs())} to focus
</Text>
) : null}
</View>
{/* Button row */}
@@ -1313,6 +1324,14 @@ const styles = StyleSheet.create(((theme: any) => ({
textInputScrollWrapper: {
position: "relative",
},
focusHintText: {
position: "absolute",
top: 0,
right: 0,
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
opacity: 0.5,
},
textInput: {
width: "100%",
color: theme.colors.foreground,
@@ -279,7 +279,7 @@ export function WorkspaceSetupDialog() {
<Composer
agentId={`workspace-setup:${serverId}:${sourceDirectory}`}
serverId={serverId}
isInputActive={true}
isPaneFocused={true}
onSubmitMessage={handleCreateChatAgent}
isSubmitLoading={pendingAction === "chat"}
blurOnSubmit={true}
@@ -59,7 +59,7 @@ export function useAgentAttentionClear({
return;
}
deferredFocusEntryClearRef.current = false;
client.clearAgentAttention(resolvedAgentId);
client.clearAgentAttention(resolvedAgentId).catch(() => {});
},
[agentId, attentionReason, client, isConnected, requiresAttention],
);
@@ -9,10 +9,6 @@ import {
import { getHostRuntimeStore } from "@/runtime/host-runtime";
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
import {
summarizeSidebarProjects,
summarizeWorkspaceCollection,
} from "@/utils/workspace-fetch-debug";
const EMPTY_ORDER: string[] = [];
const EMPTY_PROJECTS: SidebarProjectEntry[] = [];
@@ -302,14 +298,6 @@ export function useSidebarWorkspacesList(options?: {
if (!serverId) {
return;
}
console.log("[WorkspaceFetch][sidebar] model", {
serverId,
connectionStatus,
hasHydratedWorkspaces,
sessionWorkspaces: summarizeWorkspaceCollection(sessionWorkspaces?.values()),
projects: summarizeSidebarProjects(projects),
});
}, [connectionStatus, hasHydratedWorkspaces, projects, serverId, sessionWorkspaces]);
useEffect(() => {
@@ -355,21 +343,10 @@ export function useSidebarWorkspacesList(options?: {
let cursor: string | null = null;
try {
while (true) {
console.log("[WorkspaceFetch][sidebar-refresh] request", {
serverId,
cursor,
existingWorkspaces: summarizeWorkspaceCollection(existingWorkspaces?.values()),
});
const payload = await client.fetchWorkspaces({
sort: [{ key: "activity_at", direction: "desc" }],
page: cursor ? { limit: 200, cursor } : { limit: 200 },
});
console.log("[WorkspaceFetch][sidebar-refresh] response", {
serverId,
cursor,
pageInfo: payload.pageInfo,
payload: summarizeWorkspaceCollection(payload.entries),
});
for (const entry of payload.entries) {
const workspace = toWorkspaceDescriptor(entry);
next.set(
@@ -388,10 +365,6 @@ export function useSidebarWorkspacesList(options?: {
const store = useSessionStore.getState();
store.setWorkspaces(serverId, next);
store.setHasHydratedWorkspaces(serverId, true);
console.log("[WorkspaceFetch][sidebar-refresh] applied", {
serverId,
nextWorkspaces: summarizeWorkspaceCollection(next.values()),
});
} catch (error) {
console.error("[WorkspaceFetch][sidebar-refresh] failed", {
serverId,
@@ -788,6 +788,32 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
},
// --- Message input ---
{
id: "message-input-focus-cmd-l-mac",
action: "message-input.action",
combo: "Cmd+L",
when: { mac: true, commandCenter: false },
payload: { type: "message-input", kind: "focus" },
help: {
id: "focus-message-input",
section: "agent-input",
label: "Focus message input",
keys: ["mod", "L"],
},
},
{
id: "message-input-focus-ctrl-l-non-mac",
action: "message-input.action",
combo: "Ctrl+L",
when: { mac: false, commandCenter: false, terminal: false },
payload: { type: "message-input", kind: "focus" },
help: {
id: "focus-message-input",
section: "agent-input",
label: "Focus message input",
keys: ["mod", "L"],
},
},
{
id: "message-input-voice-toggle-cmd-shift-d-mac",
action: "message-input.action",
+1 -1
View File
@@ -965,7 +965,7 @@ function ChatAgentContent({
<Composer
agentId={agentId}
serverId={serverId}
isInputActive={isPaneFocused}
isPaneFocused={isPaneFocused}
value={agentInputDraft.text}
onChangeText={agentInputDraft.setText}
images={agentInputDraft.images}
@@ -1195,7 +1195,7 @@ function DraftAgentScreenContent({
<Composer
agentId={draftAgentIdRef.current}
serverId={selectedServerId ?? ""}
isInputActive={isFocused}
isPaneFocused={isFocused}
onSubmitMessage={handleCreateFromInput}
isSubmitLoading={isSubmitting}
blurOnSubmit={true}
@@ -287,7 +287,7 @@ export function NewWorkspaceScreen({
<Composer
agentId={`new-workspace:${serverId}:${sourceDirectory}`}
serverId={serverId}
isInputActive={true}
isPaneFocused={true}
onSubmitMessage={handleCreateChatAgent}
hasExternalContent={selectedGithubItem !== null}
allowEmptySubmit={true}
@@ -285,7 +285,7 @@ export function WorkspaceDraftAgentTab({
<Composer
agentId={tabId}
serverId={serverId}
isInputActive={isPaneFocused}
isPaneFocused={isPaneFocused}
onSubmitMessage={handleCreateFromInput}
isSubmitLoading={isSubmitting}
blurOnSubmit={true}