feat: editable commands

This commit is contained in:
Alejandro Gómez
2025-12-13 22:53:27 +01:00
parent 92cb290c4d
commit d877e51317
13 changed files with 1076 additions and 57 deletions

View File

@@ -0,0 +1,16 @@
import { atom } from "jotai";
/**
* Edit mode state for CommandLauncher.
* When set, CommandLauncher opens in edit mode for the specified window.
*/
export interface EditModeState {
windowId: string;
initialCommand: string;
}
/**
* Atom to control edit mode in CommandLauncher.
* Set this to trigger edit mode, null for normal create mode.
*/
export const commandLauncherEditModeAtom = atom<EditModeState | null>(null);

View File

@@ -30,7 +30,7 @@ export const createWorkspace = (
*/
export const addWindow = (
state: GrimoireState,
payload: { appId: string; title: string; props: any },
payload: { appId: string; title: string; props: any; commandString?: string },
): GrimoireState => {
const activeId = state.activeWorkspaceId;
const ws = state.workspaces[activeId];
@@ -40,6 +40,7 @@ export const addWindow = (
appId: payload.appId as any,
title: payload.title,
props: payload.props,
commandString: payload.commandString,
};
// Simple Binary Split Logic
@@ -263,3 +264,56 @@ export const setActiveAccountRelays = (
},
};
};
/**
* Deletes a workspace by ID.
* Cannot delete the last remaining workspace.
* Does NOT change activeWorkspaceId - caller is responsible for workspace navigation.
*/
export const deleteWorkspace = (
state: GrimoireState,
workspaceId: string,
): GrimoireState => {
const workspaceIds = Object.keys(state.workspaces);
// Don't delete if it's the only workspace
if (workspaceIds.length <= 1) {
return state;
}
// Don't delete if workspace doesn't exist
if (!state.workspaces[workspaceId]) {
return state;
}
// Remove the workspace (don't touch activeWorkspaceId - that's the caller's job)
const { [workspaceId]: _removed, ...remainingWorkspaces } = state.workspaces;
return {
...state,
workspaces: remainingWorkspaces,
};
};
/**
* Updates an existing window with new properties.
* Allows updating props, title, commandString, and even appId (which changes the viewer type).
*/
export const updateWindow = (
state: GrimoireState,
windowId: string,
updates: Partial<Pick<WindowInstance, "props" | "title" | "commandString" | "appId">>,
): GrimoireState => {
const window = state.windows[windowId];
if (!window) {
return state; // Window doesn't exist, return unchanged
}
return {
...state,
windows: {
...state.windows,
[windowId]: { ...window, ...updates },
},
};
};

View File

@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useAtom } from "jotai";
import { atomWithStorage, createJSONStorage } from "jotai/utils";
import { GrimoireState, AppId } from "@/types/app";
import { GrimoireState, AppId, WindowInstance } from "@/types/app";
import { useLocale } from "@/hooks/useLocale";
import * as Logic from "./logic";
@@ -82,14 +82,17 @@ export const useGrimoire = () => {
const count = Object.keys(state.workspaces).length + 1;
setState((prev) => Logic.createWorkspace(prev, count.toString()));
},
addWindow: (appId: AppId, props: any, title?: string) =>
addWindow: (appId: AppId, props: any, title?: string, commandString?: string) =>
setState((prev) =>
Logic.addWindow(prev, {
appId,
props,
title: title || appId.toUpperCase(),
commandString,
}),
),
updateWindow: (windowId: string, updates: Partial<Pick<WindowInstance, "props" | "title" | "commandString" | "appId">>) =>
setState((prev) => Logic.updateWindow(prev, windowId, updates)),
removeWindow: (id: string) =>
setState((prev) => Logic.removeWindow(prev, id)),
moveWindowToWorkspace: (windowId: string, targetWorkspaceId: string) =>
@@ -99,7 +102,37 @@ export const useGrimoire = () => {
updateLayout: (layout: any) =>
setState((prev) => Logic.updateLayout(prev, layout)),
setActiveWorkspace: (id: string) =>
setState((prev) => ({ ...prev, activeWorkspaceId: id })),
setState((prev) => {
// Validate target workspace exists
if (!prev.workspaces[id]) {
console.warn(`Cannot switch to non-existent workspace: ${id}`);
return prev;
}
// If not actually switching, return unchanged
if (prev.activeWorkspaceId === id) {
return prev;
}
// Check if we're leaving an empty workspace and should auto-remove it
const currentWorkspace = prev.workspaces[prev.activeWorkspaceId];
const shouldDeleteCurrent =
currentWorkspace &&
currentWorkspace.windowIds.length === 0 &&
Object.keys(prev.workspaces).length > 1;
if (shouldDeleteCurrent) {
// Delete the empty workspace, then switch to target
const afterDelete = Logic.deleteWorkspace(
prev,
prev.activeWorkspaceId,
);
return { ...afterDelete, activeWorkspaceId: id };
}
// Normal workspace switch
return { ...prev, activeWorkspaceId: id };
}),
setActiveAccount: (pubkey: string | undefined) =>
setState((prev) => Logic.setActiveAccount(prev, pubkey)),
setActiveAccountRelays: (relays: any) =>