Edit workspace files directly on web (#2270)

* feat(files): edit workspace files on web

Keep source buffers synchronized with host file changes and require an explicit overwrite or reload when revisions diverge.

* feat(panels): surface and protect modified tabs

Expose tooltip and modification state through the generic panel boundary so tabs can show stable metadata and guard every close route consistently.

* fix(tests): use portable fake timeout handle

* fix(files): harden editor conflict handling

Preserve modified panel state across tab eviction, use precise revisions for optimistic writes, coalesce concurrent file watchers, and localize the editor interface.

* fix(files): close editor concurrency gaps

Coalesce clean reloads, preserve subscriber identities and file permissions, suspend pending saves during close confirmation, and carry precise revisions through file reads.

* test(files): expect read revision metadata
This commit is contained in:
Mohamed Boudra
2026-07-21 04:41:19 +08:00
committed by GitHub
parent 9292f58896
commit 4bda2dfea9
69 changed files with 3678 additions and 241 deletions
@@ -553,12 +553,14 @@ function TabChip({
onCloseTab: (tabId: string) => Promise<void> | void;
dragHandleProps: DraggableListDragHandleProps | undefined;
}) {
const { t } = useTranslation();
const { closeButtonTestId, contextMenuTestId, menuEntries } = resolvedTab;
const middleClickRef = useMiddleClickClose(
useCallback(() => void onCloseTab(tab.tabId), [onCloseTab, tab.tabId]),
);
const [hovered, setHovered] = useState(false);
const isHighlighted = isActive || hovered || isCloseHovered;
const showTrailingAffordance = showCloseButton || presentation.modified;
const closeButtonDragBlockers = isWeb
? ({
onPointerDown: (event: { stopPropagation?: () => void }) => {
@@ -630,16 +632,19 @@ function TabChip({
[isFocused],
);
const tabLabelSkeletonStyle = useMemo(
() => [styles.tabLabelSkeleton, showCloseButton && styles.tabLabelSkeletonWithCloseButton],
[showCloseButton],
() => [
styles.tabLabelSkeleton,
showTrailingAffordance && styles.tabLabelSkeletonWithCloseButton,
],
[showTrailingAffordance],
);
const tabLabelStyle = useMemo(
() => [
styles.tabLabel,
isHighlighted && styles.tabLabelActive,
showCloseButton && styles.tabLabelWithCloseButton,
showTrailingAffordance && styles.tabLabelWithCloseButton,
],
[isHighlighted, showCloseButton],
[isHighlighted, showTrailingAffordance],
);
return (
@@ -672,7 +677,7 @@ function TabChip({
tabLabelStyle={tabLabelStyle}
/>
{showCloseButton ? (
{showTrailingAffordance ? (
<Pressable
{...(closeButtonDragBlockers as object | undefined)}
testID={closeButtonTestId}
@@ -683,28 +688,43 @@ function TabChip({
onPress={handleCloseButtonPress}
style={closeButtonStyle}
>
{({ hovered: closeHovered, pressed }) =>
isClosingTab ? (
<ThemedActivityIndicator
size={12}
uniProps={
closeHovered || pressed ? foregroundColorMapping : mutedColorMapping
}
{({ hovered: closeHovered, pressed }) => {
const highlighted = closeHovered || pressed;
if (isClosingTab) {
return (
<ThemedActivityIndicator
size={12}
uniProps={highlighted ? foregroundColorMapping : mutedColorMapping}
/>
);
}
if (highlighted || !presentation.modified) {
return (
<ThemedX
size={12}
uniProps={highlighted ? foregroundColorMapping : mutedColorMapping}
/>
);
}
return (
<View
style={styles.tabModifiedDot}
accessibilityLabel={t("workspace.tabs.modified")}
testID={`workspace-tab-modified-${buildDeterministicWorkspaceTabId(tab.target)}`}
/>
) : (
<ThemedX
size={12}
uniProps={
closeHovered || pressed ? foregroundColorMapping : mutedColorMapping
}
/>
)
}
);
}}
</Pressable>
) : null}
</ContextMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<TooltipContent
side="bottom"
align="center"
offset={8}
maxWidth={720}
testID={`workspace-tab-tooltip-${buildDeterministicWorkspaceTabId(tab.target)}`}
>
{tab.target.kind === "agent" ? (
<View style={styles.tooltipAgentRow}>
<Text style={styles.newTabTooltipText}>{tooltipLabel}</Text>
@@ -1161,7 +1181,7 @@ function ResolvedDesktopTabChip({
const tooltipLabel =
presentation.titleState === "loading"
? t("workspace.tabs.loadingAgentTitle")
: presentation.label;
: presentation.tooltip;
return (
<View style={styles.tabSlot}>
@@ -1331,6 +1351,12 @@ const styles = StyleSheet.create((theme) => ({
tabCloseButtonActive: {
backgroundColor: theme.colors.surface3,
},
tabModifiedDot: {
width: 8,
height: 8,
borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.foregroundMuted,
},
newTabActionButton: {
width: 22,
height: 22,