feat: sortable workspace tabs, tab context menus, and terminal auto-create removal

- Add drag-and-drop tab reordering with persistent tab order
- Add right-click context menu with copy agent ID, copy resume command, close to right
- Switch workspace route encoding from percent-encoding to base64url
- Remove terminal auto-creation on directory listing (explicit create only)
- Add swipe gestures on terminal emulator for mobile sidebar navigation
- Highlight active workspace row in sidebar
This commit is contained in:
Mohamed Boudra
2026-03-02 15:59:13 +07:00
parent c8cef9bec8
commit 223c8d7d0b
19 changed files with 1971 additions and 285 deletions
@@ -6,6 +6,22 @@ export type WorkspaceTabTarget =
| { kind: "agent"; agentId: string }
| { kind: "terminal"; terminalId: string };
function normalizeKeys(keys: string[]): string[] {
const seen = new Set<string>();
const normalized: string[] = [];
for (const rawKey of keys) {
const key = rawKey.trim();
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
normalized.push(key);
}
return normalized;
}
function trimNonEmpty(value: string | null | undefined): string | null {
if (typeof value !== "string") {
return null;
@@ -51,6 +67,7 @@ export function buildWorkspaceTabPersistenceKey(input: {
type WorkspaceTabsState = {
lastFocusedTabByWorkspace: Record<string, WorkspaceTabTarget>;
tabOrderByWorkspace: Record<string, string[]>;
setLastFocusedTab: (input: {
serverId: string;
workspaceId: string;
@@ -60,12 +77,18 @@ type WorkspaceTabsState = {
serverId: string;
workspaceId: string;
}) => WorkspaceTabTarget | null;
setTabOrder: (input: {
serverId: string;
workspaceId: string;
keys: string[];
}) => void;
};
export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
persist(
(set, get) => ({
lastFocusedTabByWorkspace: {},
tabOrderByWorkspace: {},
setLastFocusedTab: ({ serverId, workspaceId, tab }) => {
const key = buildWorkspaceTabPersistenceKey({ serverId, workspaceId });
const normalizedTab = normalizeWorkspaceTab(tab);
@@ -103,15 +126,45 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
const value = get().lastFocusedTabByWorkspace[key];
return normalizeWorkspaceTab(value);
},
setTabOrder: ({ serverId, workspaceId, keys }) => {
const key = buildWorkspaceTabPersistenceKey({ serverId, workspaceId });
if (!key) {
return;
}
const normalized = normalizeKeys(keys);
set((state) => {
const current = state.tabOrderByWorkspace[key] ?? [];
if (current.length === normalized.length) {
let isSame = true;
for (let index = 0; index < current.length; index += 1) {
if (current[index] !== normalized[index]) {
isSame = false;
break;
}
}
if (isSame) {
return state;
}
}
return {
tabOrderByWorkspace: {
...state.tabOrderByWorkspace,
[key]: normalized,
},
};
});
},
}),
{
name: "workspace-tabs-state",
version: 1,
version: 2,
storage: createJSONStorage(() => AsyncStorage),
migrate: (persistedState) => {
const state = persistedState as
| {
lastFocusedTabByWorkspace?: Record<string, WorkspaceTabTarget>;
tabOrderByWorkspace?: Record<string, string[]>;
}
| undefined;
@@ -126,9 +179,23 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
}
}
const rawOrder = state?.tabOrderByWorkspace ?? {};
const nextOrder: Record<string, string[]> = {};
for (const key in rawOrder) {
const list = rawOrder[key];
if (!Array.isArray(list)) {
continue;
}
const normalized = normalizeKeys(list.map((value) => String(value)));
if (normalized.length > 0) {
nextOrder[key] = normalized;
}
}
return {
...state,
lastFocusedTabByWorkspace: next,
tabOrderByWorkspace: nextOrder,
};
},
}