refactor: extract workspace terminal sessions, improve sidebar drag-and-drop with nestable lists, and consolidate tab presentation logic
This commit is contained in:
@@ -9,7 +9,7 @@ describe("decideLongPressMove", () => {
|
||||
it("keeps long press pending for small movement before long-press arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
longPressArmed: false,
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 3, y: 2 },
|
||||
@@ -20,18 +20,51 @@ describe("decideLongPressMove", () => {
|
||||
it("cancels long press when movement exceeds cancel slop before arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
longPressArmed: false,
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 12, y: 0 },
|
||||
currentPoint: { x: 8, y: 8 },
|
||||
})
|
||||
).toBe("cancel_long_press");
|
||||
});
|
||||
|
||||
it("yields to vertical scroll before drag arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 2, y: 7 },
|
||||
})
|
||||
).toBe("vertical_scroll");
|
||||
});
|
||||
|
||||
it("keeps diagonal motion neutral before drag arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 5, y: 7 },
|
||||
})
|
||||
).toBe("none");
|
||||
});
|
||||
|
||||
it("yields to horizontal swipe before drag arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: -10, y: 2 },
|
||||
})
|
||||
).toBe("horizontal_swipe");
|
||||
});
|
||||
|
||||
it("starts drag when movement exceeds drag slop after long-press arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
longPressArmed: true,
|
||||
dragArmed: true,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 0, y: 9 },
|
||||
@@ -42,7 +75,7 @@ describe("decideLongPressMove", () => {
|
||||
it("does nothing when drag already started", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
longPressArmed: true,
|
||||
dragArmed: true,
|
||||
didStartDrag: true,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 20, y: 20 },
|
||||
@@ -75,4 +108,3 @@ describe("shouldOpenContextMenuOnPressOut", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
export type LongPressMoveDecision = "none" | "cancel_long_press" | "start_drag";
|
||||
export type LongPressMoveDecision =
|
||||
| "none"
|
||||
| "cancel_long_press"
|
||||
| "vertical_scroll"
|
||||
| "horizontal_swipe"
|
||||
| "start_drag";
|
||||
|
||||
export function decideLongPressMove(input: {
|
||||
longPressArmed: boolean;
|
||||
dragArmed: boolean;
|
||||
didStartDrag: boolean;
|
||||
startPoint: { x: number; y: number } | null;
|
||||
currentPoint: { x: number; y: number };
|
||||
cancelSlopPx?: number;
|
||||
scrollSlopPx?: number;
|
||||
swipeSlopPx?: number;
|
||||
directionalDominanceRatio?: number;
|
||||
dragSlopPx?: number;
|
||||
}): LongPressMoveDecision {
|
||||
const cancelSlopPx = input.cancelSlopPx ?? 10;
|
||||
const scrollSlopPx = input.scrollSlopPx ?? 6;
|
||||
const swipeSlopPx = input.swipeSlopPx ?? 8;
|
||||
const directionalDominanceRatio = input.directionalDominanceRatio ?? 1.5;
|
||||
const dragSlopPx = input.dragSlopPx ?? 8;
|
||||
|
||||
if (!input.startPoint) {
|
||||
@@ -20,9 +31,19 @@ export function decideLongPressMove(input: {
|
||||
|
||||
const dx = input.currentPoint.x - input.startPoint.x;
|
||||
const dy = input.currentPoint.y - input.startPoint.y;
|
||||
const absDx = Math.abs(dx);
|
||||
const absDy = Math.abs(dy);
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const clearlyVertical = absDy >= absDx * directionalDominanceRatio;
|
||||
const clearlyHorizontal = absDx >= absDy * directionalDominanceRatio;
|
||||
|
||||
if (!input.longPressArmed) {
|
||||
if (!input.dragArmed) {
|
||||
if (clearlyVertical && absDy > scrollSlopPx) {
|
||||
return "vertical_scroll";
|
||||
}
|
||||
if (clearlyHorizontal && absDx > swipeSlopPx) {
|
||||
return "horizontal_swipe";
|
||||
}
|
||||
return distance > cancelSlopPx ? "cancel_long_press" : "none";
|
||||
}
|
||||
return distance > dragSlopPx ? "start_drag" : "none";
|
||||
@@ -34,4 +55,3 @@ export function shouldOpenContextMenuOnPressOut(input: {
|
||||
}): boolean {
|
||||
return input.longPressArmed && !input.didStartDrag;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,23 +35,29 @@ describe("terminal-attach", () => {
|
||||
it("reads and updates resume offsets monotonically", () => {
|
||||
const offsets = new Map<string, number>();
|
||||
const terminalId = "term-1";
|
||||
const resumeOffsetStore = {
|
||||
get: ({ terminalId }: { terminalId: string }) => offsets.get(terminalId),
|
||||
set: ({ terminalId, offset }: { terminalId: string; offset: number }) => {
|
||||
offsets.set(terminalId, offset);
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
getTerminalResumeOffset({
|
||||
terminalId,
|
||||
resumeOffsetByTerminalId: offsets,
|
||||
resumeOffsetStore,
|
||||
})
|
||||
).toBeUndefined();
|
||||
|
||||
updateTerminalResumeOffset({
|
||||
terminalId,
|
||||
offset: 8,
|
||||
resumeOffsetByTerminalId: offsets,
|
||||
resumeOffsetStore,
|
||||
});
|
||||
expect(
|
||||
getTerminalResumeOffset({
|
||||
terminalId,
|
||||
resumeOffsetByTerminalId: offsets,
|
||||
resumeOffsetStore,
|
||||
})
|
||||
).toBe(8);
|
||||
|
||||
@@ -59,12 +65,12 @@ describe("terminal-attach", () => {
|
||||
updateTerminalResumeOffset({
|
||||
terminalId,
|
||||
offset: 3,
|
||||
resumeOffsetByTerminalId: offsets,
|
||||
resumeOffsetStore,
|
||||
});
|
||||
expect(
|
||||
getTerminalResumeOffset({
|
||||
terminalId,
|
||||
resumeOffsetByTerminalId: offsets,
|
||||
resumeOffsetStore,
|
||||
})
|
||||
).toBe(8);
|
||||
});
|
||||
|
||||
@@ -8,11 +8,16 @@ const TERMINAL_ATTACH_RETRYABLE_ERROR_PATTERNS = [
|
||||
"stream ended",
|
||||
] as const;
|
||||
|
||||
export type TerminalResumeOffsetStore = {
|
||||
get: (input: { terminalId: string }) => number | undefined;
|
||||
set: (input: { terminalId: string; offset: number }) => void;
|
||||
};
|
||||
|
||||
export function getTerminalResumeOffset(input: {
|
||||
terminalId: string;
|
||||
resumeOffsetByTerminalId: Map<string, number>;
|
||||
resumeOffsetStore: TerminalResumeOffsetStore;
|
||||
}): number | undefined {
|
||||
const offset = input.resumeOffsetByTerminalId.get(input.terminalId);
|
||||
const offset = input.resumeOffsetStore.get({ terminalId: input.terminalId });
|
||||
if (typeof offset !== "number" || !Number.isFinite(offset)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -24,7 +29,7 @@ export function getTerminalResumeOffset(input: {
|
||||
export function updateTerminalResumeOffset(input: {
|
||||
terminalId: string;
|
||||
offset: number;
|
||||
resumeOffsetByTerminalId: Map<string, number>;
|
||||
resumeOffsetStore: TerminalResumeOffsetStore;
|
||||
}): void {
|
||||
if (!Number.isFinite(input.offset)) {
|
||||
return;
|
||||
@@ -34,13 +39,16 @@ export function updateTerminalResumeOffset(input: {
|
||||
const previousOffset =
|
||||
getTerminalResumeOffset({
|
||||
terminalId: input.terminalId,
|
||||
resumeOffsetByTerminalId: input.resumeOffsetByTerminalId,
|
||||
resumeOffsetStore: input.resumeOffsetStore,
|
||||
}) ?? -1;
|
||||
if (normalizedOffset <= previousOffset) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.resumeOffsetByTerminalId.set(input.terminalId, normalizedOffset);
|
||||
input.resumeOffsetStore.set({
|
||||
terminalId: input.terminalId,
|
||||
offset: normalizedOffset,
|
||||
});
|
||||
}
|
||||
|
||||
export function getTerminalAttachRetryDelayMs(input: { attempt: number }): number {
|
||||
|
||||
Reference in New Issue
Block a user