Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7a9e57b
feat(desktop): auto-name setup workspace terminal to "Workspace Setup"
Kitenite Dec 9, 2025
02253a9
refactor(desktop): use paneId for terminal sessions and pass all 3 ID…
Kitenite Dec 10, 2025
b461943
lint
Kitenite Dec 10, 2025
1b5a51b
prefer tab
Kitenite Dec 10, 2025
f5284b6
fix(desktop): filter both dev and prod bin dirs in findRealBinary
Kitenite Dec 10, 2025
4e7789c
refactor(desktop): use lowdb for tabs state persistence
Kitenite Dec 10, 2025
97a4257
refactor(desktop): separate lowdb instance for app-state
Kitenite Dec 10, 2025
ea8efbb
clean up
Kitenite Dec 10, 2025
1f22e8a
refactor(desktop): extract shared tabs types to shared/tabs-types.ts
Kitenite Dec 10, 2025
30362be
fix(desktop): handle JSON parse errors in lowdb-storage
Kitenite Dec 10, 2025
a68d814
refactor(desktop): migrate storage to tRPC, remove electron-store
Kitenite Dec 10, 2025
424f258
clean up
Kitenite Dec 10, 2025
bde3e3e
fix(desktop): fix appState.update() and add layout to tab schema
Kitenite Dec 10, 2025
e6d67f8
fix(desktop): handle legacy app-state.json format in initAppState
Kitenite Dec 10, 2025
b055fc6
fix(desktop): add null guards for tabsState access in notifications
Kitenite Dec 10, 2025
3d9f2da
Merge main: resolve conflicts in terminal-manager and Terminal.tsx
Kitenite Dec 10, 2025
45e7770
refactor(desktop): rename tabId to paneId for consistency
Kitenite Dec 10, 2025
94a4bfc
fix(desktop): add defensive guards for db/appState in notification ha…
Kitenite Dec 10, 2025
c1a63b9
fix lint
Kitenite Dec 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
"dnd-core": "^16.0.1",
"dotenv": "^17.2.3",
"electron-router-dom": "^2.1.0",
"electron-store": "^11.0.2",
"electron-updater": "6",
"execa": "^9.6.0",
"express": "^5.1.0",
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createNotificationsRouter } from "./notifications";
import { createProjectsRouter } from "./projects";
import { createSettingsRouter } from "./settings";
import { createTerminalRouter } from "./terminal";
import { createUiStateRouter } from "./ui-state";
import { createWindowRouter } from "./window";
import { createWorkspacesRouter } from "./workspaces";

Expand All @@ -28,6 +29,7 @@ export const createAppRouter = (getWindow: () => BrowserWindow | null) => {
external: createExternalRouter(),
settings: createSettingsRouter(),
config: createConfigRouter(),
uiState: createUiStateRouter(),
});
};

Expand Down
11 changes: 9 additions & 2 deletions apps/desktop/src/lib/trpc/routers/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { publicProcedure, router } from "..";

type NotificationEvent =
| { type: "agent-complete"; data: AgentCompleteEvent }
| { type: "focus-tab"; data: { tabId: string; workspaceId: string } };
| {
type: "focus-tab";
data: { paneId: string; tabId: string; workspaceId: string };
};

export const createNotificationsRouter = () => {
return router({
Expand All @@ -20,7 +23,11 @@ export const createNotificationsRouter = () => {
emit.next({ type: "agent-complete", data: event });
};

const onFocusTab = (data: { tabId: string; workspaceId: string }) => {
const onFocusTab = (data: {
paneId: string;
tabId: string;
workspaceId: string;
}) => {
emit.next({ type: "focus-tab", data });
};

Expand Down
69 changes: 25 additions & 44 deletions apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,23 @@ import { resolveCwd } from "./utils";

/**
* Terminal router using TerminalManager with node-pty
* Sessions are keyed by tabId and linked to workspaces for cwd resolution
* Sessions are keyed by paneId and linked to workspaces for cwd resolution
*
* IMPORTANT: When creating terminals, ensure these env vars are passed:
* - PATH: Prepend ~/.superset/bin (use getSupersetBinDir() from agent-setup)
* - SUPERSET_TAB_ID: The tab's ID
* - SUPERSET_TAB_TITLE: The tab's display title
* - SUPERSET_WORKSPACE_NAME: The workspace name
* - SUPERSET_PORT: The hooks server port (use getHooksServerPort())
*
* PATH prepending ensures our wrapper scripts (~/.superset/bin/claude, codex)
* are used instead of system binaries. These wrappers inject hook settings
* that notify the app when agents complete their tasks.
* Environment variables set for terminal sessions:
* - PATH: Prepends ~/.superset/bin so wrapper scripts intercept agent commands
* - SUPERSET_PANE_ID: The pane ID (used by notification hooks, session key)
* - SUPERSET_TAB_ID: The tab ID (parent of pane, used by notification hooks)
* - SUPERSET_WORKSPACE_ID: The workspace ID (used by notification hooks)
* - SUPERSET_PORT: The hooks server port for agent completion notifications
*/
export const createTerminalRouter = () => {
return router({
createOrAttach: publicProcedure
.input(
z.object({
paneId: z.string(),
tabId: z.string(),
workspaceId: z.string(),
tabTitle: z.string(),
cols: z.number().optional(),
rows: z.number().optional(),
cwd: z.string().optional(),
Expand All @@ -37,49 +33,34 @@ export const createTerminalRouter = () => {
)
.mutation(async ({ input }) => {
const {
paneId,
tabId,
workspaceId,
tabTitle,
cols,
rows,
cwd: cwdOverride,
initialCommands,
} = input;

// Get workspace to determine cwd and workspace name
const workspace = db.data.workspaces.find((w) => w.id === workspaceId);
const worktree = workspace
? db.data.worktrees.find((wt) => wt.id === workspace.worktreeId)
: undefined;
const workspaceName =
workspace?.name || worktree?.branch || "Workspace";

// Resolve cwd: absolute paths stay as-is, relative paths resolve against worktree
const workspace = db.data.workspaces.find((w) => w.id === workspaceId);
const worktreePath = workspace
? getWorktreePath(workspace.worktreeId)
: undefined;
const cwd = resolveCwd(cwdOverride, worktreePath);

// Get project to get root path for setup scripts
const project = workspace
? db.data.projects.find((p) => p.id === workspace.projectId)
: undefined;
const rootPath = project?.mainRepoPath;

const result = await terminalManager.createOrAttach({
paneId,
tabId,
workspaceId,
tabTitle,
workspaceName,
rootPath,
cwd,
cols,
rows,
initialCommands,
});

return {
tabId,
paneId,
isNew: result.isNew,
scrollback: result.scrollback,
wasRecovered: result.wasRecovered,
Expand All @@ -89,7 +70,7 @@ export const createTerminalRouter = () => {
write: publicProcedure
.input(
z.object({
tabId: z.string(),
paneId: z.string(),
data: z.string(),
}),
)
Expand All @@ -100,7 +81,7 @@ export const createTerminalRouter = () => {
resize: publicProcedure
.input(
z.object({
tabId: z.string(),
paneId: z.string(),
cols: z.number(),
rows: z.number(),
seq: z.number().optional(),
Expand All @@ -113,7 +94,7 @@ export const createTerminalRouter = () => {
signal: publicProcedure
.input(
z.object({
tabId: z.string(),
paneId: z.string(),
signal: z.string().optional(),
}),
)
Expand All @@ -124,7 +105,7 @@ export const createTerminalRouter = () => {
kill: publicProcedure
.input(
z.object({
tabId: z.string(),
paneId: z.string(),
deleteHistory: z.boolean().optional(),
}),
)
Expand All @@ -138,7 +119,7 @@ export const createTerminalRouter = () => {
detach: publicProcedure
.input(
z.object({
tabId: z.string(),
paneId: z.string(),
}),
)
.mutation(async ({ input }) => {
Expand All @@ -152,7 +133,7 @@ export const createTerminalRouter = () => {
clearScrollback: publicProcedure
.input(
z.object({
tabId: z.string(),
paneId: z.string(),
}),
)
.mutation(async ({ input }) => {
Expand All @@ -161,8 +142,8 @@ export const createTerminalRouter = () => {

getSession: publicProcedure
.input(z.string())
.query(async ({ input: tabId }) => {
return terminalManager.getSession(tabId);
.query(async ({ input: paneId }) => {
return terminalManager.getSession(paneId);
}),

/**
Expand All @@ -185,7 +166,7 @@ export const createTerminalRouter = () => {

stream: publicProcedure
.input(z.string())
.subscription(({ input: tabId }) => {
.subscription(({ input: paneId }) => {
return observable<
| { type: "data"; data: string }
| { type: "exit"; exitCode: number; signal?: number }
Expand All @@ -199,13 +180,13 @@ export const createTerminalRouter = () => {
emit.complete();
};

terminalManager.on(`data:${tabId}`, onData);
terminalManager.on(`exit:${tabId}`, onExit);
terminalManager.on(`data:${paneId}`, onData);
terminalManager.on(`exit:${paneId}`, onExit);

// Cleanup on unsubscribe
return () => {
terminalManager.off(`data:${tabId}`, onData);
terminalManager.off(`exit:${tabId}`, onExit);
terminalManager.off(`data:${paneId}`, onData);
terminalManager.off(`exit:${paneId}`, onExit);
};
});
}),
Expand Down
Loading