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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user