@polderlabs/bizar-omp 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,14 +141,18 @@ The `omb agents` hub provides:
141
141
  - A global registry of every Bizar session, including sessions running outside the directory where `omb agents` was opened.
142
142
  - Session attach, rename, delete, refresh, settings, help, and reconnect controls.
143
143
  - A new-session selector opened with `n`.
144
- - Fuzzy directory search, parent and child navigation, recent project shortcuts, and direct absolute paths.
145
- - A configurable Projects folder in Settings; its child directories are indexed for quick new-session dispatch from anywhere.
144
+ - `Tab` focus for the workspace rail. Select a workspace with `Up`/`Down`, then press `Enter` to immediately launch a new session rooted in that workspace; use `n` for the full new-session menu.
145
+ - Workspace separators in the session list so sessions from different projects are easy to scan.
146
+ - Inline rename from the hub with `e`; the name is saved without leaving the TUI for a separate prompt.
147
+ - Mouse support for clicking sessions and workspaces and scrolling either list. The hub enables terminal SGR mouse reporting only while it is open.
148
+ - Fuzzy directory search with recursive subdirectory indexing, parent navigation, recent project shortcuts, path autocomplete, and direct absolute paths.
149
+ - A configurable Project directory root in Settings; it becomes the default new-session root and its full subdirectory tree is indexed for quick dispatch from anywhere.
146
150
  - Optional initial task text and session naming without leaving the TUI.
147
151
  - Native OMP session titles with deterministic project/task fallbacks.
148
152
 
149
- Key controls are `Enter` attach, `n` new session, `e` rename, `d` delete, `s` settings, `r` refresh, `?` help, and `q` exit. Inside a session, `Ctrl-d` or `Ctrl-c` returns to the hub without sending EOF or interrupt input to OMP. Mouse scrolling is enabled by default and can be changed in Settings.
153
+ Key controls are `Enter` attach, `Tab` focus workspaces, workspace `Enter` immediate new session, `n` full new-session menu, `e` rename, `d` delete, `s` settings, `r` refresh, `?` help, and `q` exit. Inside a session, `Ctrl-d` or `Ctrl-c` returns to the hub without sending EOF or interrupt input to OMP. Mouse interaction in the hub supports row clicks and scrolling; mouse scrolling for attached OMP sessions remains configurable in Settings.
150
154
 
151
- To configure fast project picking, open `s`, select `Projects folder`, press `Enter`, enter a path such as `~/projects`, and press `s` to save. The new-session selector keeps direct absolute paths and parent navigation, while also showing the configured folder and its immediate child directories.
155
+ To configure fast project picking, open `s`, select `Project directory root`, press `Enter`, enter a path such as `~/projects`, and press `s` to save. The new-session selector keeps direct absolute paths and parent navigation, while also indexing the configured folder's recursive subdirectories.
152
156
 
153
157
  ## Model roles and native assets
154
158
 
@@ -169,7 +173,7 @@ Bizar registers visible `bizar_*` roles without replacing operator-owned mapping
169
173
  | OMP | 18.2.6, with 18.2.5 and 18.2.4 regression-qualified |
170
174
  | Node.js | 22.x and 24.x in CI |
171
175
  | Bun | 1.3.14 compatibility job |
172
- | Package | `@polderlabs/bizar-omp@0.4.4` |
176
+ | Package | `@polderlabs/bizar-omp@0.4.5` |
173
177
 
174
178
  Upstream OMP changes require a new compatibility baseline and contract run. The package does not widen a version range to hide an unqualified runtime.
175
179
 
@@ -13,12 +13,14 @@ export type AgentsHubAction = {
13
13
  } | {
14
14
  type: "rename";
15
15
  session: OmbSessionInfo;
16
+ nextId: string;
16
17
  } | {
17
18
  type: "delete";
18
19
  session: OmbSessionInfo;
20
+ nextSessionName?: string;
19
21
  } | {
20
22
  type: "settings";
21
23
  settings: OmbSettings;
22
24
  };
23
- export declare function runInkAgentsHub(cwd: string, env: NodeJS.ProcessEnv, sessions: OmbSessionInfo[], settings: OmbSettings): Promise<AgentsHubAction>;
25
+ export declare function runInkAgentsHub(cwd: string, env: NodeJS.ProcessEnv, sessions: OmbSessionInfo[], settings: OmbSettings, initialSelectedName?: string): Promise<AgentsHubAction>;
24
26
  //# sourceMappingURL=agents-ink.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents-ink.d.ts","sourceRoot":"","sources":["../../src/cli/agents-ink.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA0C,KAAK,cAAc,EAAwB,MAAM,wBAAwB,CAAC;AAC3H,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG1D,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC;AAsfhD,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAOtJ"}
1
+ {"version":3,"file":"agents-ink.d.ts","sourceRoot":"","sources":["../../src/cli/agents-ink.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA0C,KAAK,cAAc,EAAwB,MAAM,wBAAwB,CAAC;AAC3H,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG1D,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC;AA8tBhD,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,mBAAmB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAOpL"}
@@ -1,8 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from "react";
3
- import { Box, Text, useApp, useInput, useWindowSize, render } from "ink";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Box, Text, useApp, useInput, useStdin, useStdout, useWindowSize, render } from "ink";
4
4
  import { listOmbSessions, ombSessionDisplayName } from "../omp/omb-sessions.js";
5
- import { filterDirectoryOptions, readDirectoryOptions } from "./directory-selector.js";
5
+ import { expandDirectoryPath, filterDirectoryOptions, readDirectoryCompletions, readDirectoryOptions } from "./directory-selector.js";
6
6
  const statusLabel = (state) => state === "attached" ? "CONNECTED" : state === "detached" ? "DISCONNECTED" : "STOPPED";
7
7
  const statusColor = (state) => state === "attached" ? "green" : state === "detached" ? "yellow" : "gray";
8
8
  const statusMark = (state) => state === "attached" ? "●" : state === "detached" ? "◌" : "○";
@@ -20,14 +20,25 @@ function age(session) {
20
20
  return `${Math.floor(seconds / 3600)}h ago`;
21
21
  return `${Math.floor(seconds / 86400)}d ago`;
22
22
  }
23
- function projectCounts(sessions) {
24
- const counts = new Map();
25
- for (const session of sessions)
26
- counts.set(session.project, (counts.get(session.project) ?? 0) + 1);
27
- return [...counts.entries()].sort(([a], [b]) => a.localeCompare(b));
23
+ function workspacePath(session) {
24
+ return session.cwd === "?" ? session.project : session.cwd;
25
+ }
26
+ function workspaceEntries(sessions) {
27
+ const entries = new Map();
28
+ for (const session of sessions) {
29
+ const path = workspacePath(session);
30
+ const current = entries.get(path);
31
+ if (current)
32
+ current.count += 1;
33
+ else
34
+ entries.set(path, { path, label: session.project, count: 1 });
35
+ }
36
+ return [...entries.values()].sort((a, b) => a.path.localeCompare(b.path));
28
37
  }
29
38
  function visibleSessions(allSessions, settings) {
30
- return settings.showStoppedSessions ? allSessions : allSessions.filter(session => session.state !== "dead");
39
+ const stateRank = (state) => state === "attached" ? 0 : state === "detached" ? 1 : 2;
40
+ return [...(settings.showStoppedSessions ? allSessions : allSessions.filter(session => session.state !== "dead"))]
41
+ .sort((a, b) => workspacePath(a).localeCompare(workspacePath(b)) || stateRank(a.state) - stateRank(b.state) || a.project.localeCompare(b.project) || a.name.localeCompare(b.name));
31
42
  }
32
43
  function Header({ cwd, sessions }) {
33
44
  const connected = sessions.filter(session => session.state === "attached").length;
@@ -35,9 +46,9 @@ function Header({ cwd, sessions }) {
35
46
  const stopped = sessions.filter(session => session.state === "dead").length;
36
47
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "\u25C6 BIZAR / AGENTS HUB" }), _jsxs(Text, { dimColor: true, children: ["GLOBAL \u00B7 launched from ", short(cwd, 34)] })] }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsxs(Text, { color: "green", children: ["\u25CF ", connected, " connected"] }), _jsxs(Text, { color: "yellow", children: ["\u25CC ", disconnected, " disconnected"] }), _jsxs(Text, { color: "gray", children: ["\u25CB ", stopped, " stopped"] })] })] }));
37
48
  }
38
- function WorkspaceRail({ sessions, selected }) {
39
- const projects = projectCounts(sessions);
40
- return (_jsxs(Box, { width: "28%", minWidth: 24, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "WORKSPACES" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "white", bold: true, children: ["\u203A ALL SESSIONS ", _jsx(Text, { dimColor: true, children: sessions.length })] }), projects.length === 0 ? _jsx(Text, { dimColor: true, children: " No active projects" }) : projects.map(([project, count]) => (_jsxs(Text, { color: selected?.project === project ? "cyan" : undefined, children: [selected?.project === project ? " › " : " ", short(project, 18), " ", _jsx(Text, { dimColor: true, children: count })] }, project)))] }), _jsxs(Box, { marginTop: 2, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "LIVE VIEW" }), _jsx(Text, { dimColor: true, children: "Updates every few seconds." }), _jsx(Text, { dimColor: true, children: "Only bizar-* sessions" }), _jsx(Text, { dimColor: true, children: "are shown or changed." })] })] }));
49
+ function WorkspaceRail({ sessions, selectedPath, focused }) {
50
+ const projects = workspaceEntries(sessions);
51
+ return (_jsxs(Box, { width: "28%", minWidth: 24, flexDirection: "column", borderStyle: "round", borderColor: focused ? "cyan" : "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? "white" : "cyan", children: ["WORKSPACES ", _jsx(Text, { dimColor: true, children: focused ? "· select" : "· Tab" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: focused && !selectedPath ? "white" : "cyan", bold: true, children: [focused && !selectedPath ? "›" : " ", " ALL SESSIONS ", _jsx(Text, { dimColor: true, children: sessions.length })] }), projects.length === 0 ? _jsx(Text, { dimColor: true, children: " No active projects" }) : projects.map(workspace => (_jsxs(Text, { color: selectedPath === workspace.path ? "white" : undefined, backgroundColor: focused && selectedPath === workspace.path ? "blue" : undefined, children: [selectedPath === workspace.path ? " › " : " ", short(workspace.label, 18), " ", _jsxs(Text, { dimColor: true, children: [workspace.count, " \u00B7 ", short(workspace.path, 24)] })] }, workspace.path)))] }), _jsxs(Box, { marginTop: 2, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "LIVE VIEW" }), _jsx(Text, { dimColor: true, children: "Updates every few seconds." }), _jsx(Text, { dimColor: true, children: "Only bizar-* sessions" }), _jsx(Text, { dimColor: true, children: "are shown or changed." })] })] }));
41
52
  }
42
53
  function SessionRow({ session, selected, width }) {
43
54
  const color = statusColor(session.state);
@@ -49,35 +60,47 @@ function SessionList({ sessions, selected, rows, width }) {
49
60
  const start = Math.max(0, Math.min(Math.max(0, sessions.length - visibleCount), selected - Math.floor(visibleCount / 2)));
50
61
  const visible = sessions.slice(start, start + visibleCount);
51
62
  let previous;
63
+ let previousWorkspace;
52
64
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "SESSIONS" }), _jsx(Text, { dimColor: true, children: sessions.length ? `${start + 1}-${Math.min(start + visibleCount, sessions.length)} of ${sessions.length}` : "empty" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: visible.length === 0 ? (_jsxs(Box, { flexDirection: "column", paddingY: 2, children: [_jsx(Text, { color: "yellow", children: "No Bizar sessions are running." }), _jsx(Text, { dimColor: true, children: "Press n to dispatch a new agent session." })] })) : visible.map((session, offset) => {
53
65
  const index = start + offset;
54
- const heading = session.state !== previous;
66
+ const workspace = workspacePath(session);
67
+ const workspaceChanged = workspace !== previousWorkspace;
68
+ const heading = workspaceChanged || session.state !== previous;
69
+ const workspaceHeading = workspaceChanged;
70
+ if (workspaceChanged)
71
+ previousWorkspace = workspace;
55
72
  previous = session.state;
56
- return (_jsxs(Box, { flexDirection: "column", children: [heading && _jsxs(Text, { color: statusColor(session.state), bold: true, children: [statusMark(session.state), " ", statusLabel(session.state)] }), _jsx(SessionRow, { session: session, selected: index === selected, width: width })] }, session.name));
73
+ return (_jsxs(Box, { flexDirection: "column", children: [workspaceHeading && _jsxs(Text, { color: "cyan", bold: true, children: ["\u2501\u2501 ", short(session.project, 20), " \u00B7 ", short(workspace, Math.max(20, width - 24)), " \u2501\u2501"] }), heading && _jsxs(Text, { color: statusColor(session.state), bold: true, children: [statusMark(session.state), " ", statusLabel(session.state)] }), _jsx(SessionRow, { session: session, selected: index === selected, width: width })] }, session.name));
57
74
  }) })] }));
58
75
  }
59
76
  function Inspector({ session }) {
60
77
  return (_jsxs(Box, { width: "29%", minWidth: 25, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "SELECTED SESSION" }), !session ? _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Select a session to inspect it." }) }) : (_jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: [_jsxs(Text, { bold: true, color: statusColor(session.state), children: [statusMark(session.state), " ", statusLabel(session.state)] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "NAME" }), _jsx(Text, { children: ombSessionDisplayName(session) }), _jsx(Text, { dimColor: true, children: session.nativeName ? "OMP session title" : "Generated from task or project" })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "PROJECT" }), _jsx(Text, { children: short(session.cwd, 30) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "COMMAND" }), _jsx(Text, { children: short(session.command || "omp", 30) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "LAST ACTIVITY" }), _jsx(Text, { children: age(session) })] })] }))] }));
61
78
  }
62
79
  function Footer() {
63
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "Enter" }), " attach ", _jsx(Text, { color: "cyan", children: "n" }), " new selector ", _jsx(Text, { color: "cyan", children: "e" }), " rename ", _jsx(Text, { color: "cyan", children: "d" }), " delete"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "s" }), " settings ", _jsx(Text, { color: "cyan", children: "r" }), " refresh ", _jsx(Text, { color: "cyan", children: "?" }), " help ", _jsx(Text, { color: "cyan", children: "q" }), " quit"] })] }));
80
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "Tab" }), " workspaces ", _jsx(Text, { color: "cyan", children: "Enter" }), " attach/new ", _jsx(Text, { color: "cyan", children: "n" }), " new ", _jsx(Text, { color: "cyan", children: "e" }), " rename ", _jsx(Text, { color: "cyan", children: "d" }), " delete"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "s" }), " settings ", _jsx(Text, { color: "cyan", children: "r" }), " refresh ", _jsx(Text, { color: "cyan", children: "?" }), " help ", _jsx(Text, { color: "cyan", children: "q" }), " quit"] })] }));
64
81
  }
65
82
  function Modal({ title, children, width, columns }) {
66
83
  return (_jsxs(Box, { position: "absolute", top: 2, left: Math.max(0, Math.floor((columns - width) / 2)), width: width, flexDirection: "column", borderStyle: "double", borderColor: "cyan", backgroundColor: "black", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: title }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: children })] }));
67
84
  }
68
- function NewSessionModal({ cwd, sessions, projectsFolder, selected, columns, rows, onSubmit, onClose, }) {
69
- const initialDirectory = selected?.cwd && selected.cwd !== "?" ? selected.cwd : cwd;
85
+ function NewSessionModal({ cwd, sessions, projectsFolder, initialDirectory: workspaceDirectory, selected, columns, rows, onSubmit, onClose, }) {
86
+ const initialDirectory = workspaceDirectory?.trim()
87
+ ? expandDirectoryPath(workspaceDirectory, cwd)
88
+ : projectsFolder.trim()
89
+ ? expandDirectoryPath(projectsFolder, cwd)
90
+ : selected?.cwd && selected.cwd !== "?" ? selected.cwd : cwd;
70
91
  const [mode, setMode] = useState("directory");
71
92
  const [directoryPath, setDirectoryPath] = useState(initialDirectory);
72
93
  const [directoryQuery, setDirectoryQuery] = useState("");
73
94
  const [directoryOptions, setDirectoryOptions] = useState([]);
95
+ const [pathOptions, setPathOptions] = useState([]);
74
96
  const [directorySelected, setDirectorySelected] = useState(0);
75
97
  const [projectCwd, setProjectCwd] = useState(initialDirectory);
76
98
  const [task, setTask] = useState("");
77
99
  const [sessionId, setSessionId] = useState("");
78
100
  const [loading, setLoading] = useState(true);
79
101
  const recentPaths = useMemo(() => [...new Set(sessions.map(session => session.cwd).filter(path => path && path !== "?"))], [sessions]);
80
- const filteredDirectories = useMemo(() => filterDirectoryOptions(directoryOptions, directoryQuery), [directoryOptions, directoryQuery]);
102
+ const recentPathsKey = useMemo(() => [...recentPaths].sort().join("\0"), [recentPaths]);
103
+ const filteredDirectories = useMemo(() => filterDirectoryOptions([...pathOptions, ...directoryOptions], directoryQuery), [directoryOptions, directoryQuery, pathOptions]);
81
104
  const visibleCount = Math.max(4, Math.min(9, rows - 14));
82
105
  const directoryStart = Math.max(0, Math.min(Math.max(0, filteredDirectories.length - visibleCount), directorySelected - Math.floor(visibleCount / 2)));
83
106
  const visibleDirectories = filteredDirectories.slice(directoryStart, directoryStart + visibleCount);
@@ -85,7 +108,7 @@ function NewSessionModal({ cwd, sessions, projectsFolder, selected, columns, row
85
108
  useEffect(() => {
86
109
  let active = true;
87
110
  setLoading(true);
88
- void readDirectoryOptions(directoryPath, recentPaths, directoryQuery, projectsFolder).then(options => {
111
+ void readDirectoryOptions(directoryPath, recentPaths, "", projectsFolder).then(options => {
89
112
  if (!active)
90
113
  return;
91
114
  setDirectoryOptions(options);
@@ -93,7 +116,20 @@ function NewSessionModal({ cwd, sessions, projectsFolder, selected, columns, row
93
116
  setLoading(false);
94
117
  });
95
118
  return () => { active = false; };
96
- }, [directoryPath, directoryQuery, projectsFolder, recentPaths]);
119
+ }, [directoryPath, projectsFolder, recentPathsKey]);
120
+ useEffect(() => {
121
+ setDirectorySelected(0);
122
+ if (!directoryQuery.includes("/") && !directoryQuery.startsWith("~")) {
123
+ setPathOptions([]);
124
+ return;
125
+ }
126
+ let active = true;
127
+ void readDirectoryOptions(directoryPath, [], directoryQuery).then(options => {
128
+ if (active)
129
+ setPathOptions(options.filter(option => option.kind === "path"));
130
+ });
131
+ return () => { active = false; };
132
+ }, [directoryPath, directoryQuery]);
97
133
  useEffect(() => {
98
134
  setDirectorySelected(value => Math.min(value, Math.max(0, filteredDirectories.length - 1)));
99
135
  }, [filteredDirectories.length]);
@@ -182,25 +218,50 @@ function NewSessionModal({ cwd, sessions, projectsFolder, selected, columns, row
182
218
  const index = directoryStart + offset;
183
219
  const isSelected = index === directorySelected;
184
220
  return (_jsxs(Box, { flexDirection: "column", backgroundColor: isSelected ? "blue" : undefined, children: [_jsxs(Text, { color: isSelected ? "white" : option.kind === "recent" ? "green" : "cyan", bold: isSelected, children: [isSelected ? "›" : " ", " ", option.kind === "parent" ? "↰" : option.kind === "recent" ? "●" : option.kind === "current" ? "◆" : option.kind === "projects-root" ? "▣" : option.kind === "project" ? "◇" : "□", " ", option.label] }), isSelected && _jsxs(Text, { dimColor: true, children: [" ", option.detail ?? option.path] })] }, `${option.kind}:${option.path}`));
185
- }) }), _jsxs(Text, { dimColor: true, children: [filteredDirectories.length ? `${directoryStart + 1}-${Math.min(directoryStart + visibleCount, filteredDirectories.length)} of ${filteredDirectories.length}` : "0 directories", " \u00B7 Enter choose \u2192 open \u2190 parent"] })] })) : (_jsxs(Text, { dimColor: true, children: [" ", projectCwd] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: mode === "task" ? "cyan" : undefined, bold: mode === "task", children: "2 Initial task" }), mode === "task" ? _jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { children: task || "Optional task description" })] }) : _jsxs(Text, { dimColor: true, children: [" ", task || "Optional"] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: mode === "name" ? "cyan" : undefined, bold: mode === "name", children: "3 Session name" }), mode === "name" ? _jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { children: sessionId || "Blank generates a name" })] }) : _jsxs(Text, { dimColor: true, children: [" ", sessionId || "Generated automatically"] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Type to search or edit \u00B7 Up/Down select \u00B7 Enter continue \u00B7 Esc back" }), _jsx(Text, { color: "yellow", children: mode === "directory" ? projectsFolder ? `Global sessions are listed below; ${projectsFolder} is indexed here.` : "Global sessions are listed below; configure a projects folder in settings for faster picking." : mode === "name" ? "Enter starts the durable OMB host." : "Enter moves to the session name." })] })] }) }));
221
+ }) }), _jsxs(Text, { dimColor: true, children: [filteredDirectories.length ? `${directoryStart + 1}-${Math.min(directoryStart + visibleCount, filteredDirectories.length)} of ${filteredDirectories.length}` : "0 directories", " \u00B7 Enter choose \u2192 open \u2190 parent \u00B7 fuzzy matches scan all subdirectories"] })] })) : (_jsxs(Text, { dimColor: true, children: [" ", projectCwd] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: mode === "task" ? "cyan" : undefined, bold: mode === "task", children: "2 Initial task" }), mode === "task" ? _jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { children: task || "Optional task description" })] }) : _jsxs(Text, { dimColor: true, children: [" ", task || "Optional"] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: mode === "name" ? "cyan" : undefined, bold: mode === "name", children: "3 Session name" }), mode === "name" ? _jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { children: sessionId || "Blank generates a name" })] }) : _jsxs(Text, { dimColor: true, children: [" ", sessionId || "Generated automatically"] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Type to search or edit \u00B7 Up/Down select \u00B7 Enter continue \u00B7 Esc back" }), _jsx(Text, { color: "yellow", children: mode === "directory" ? projectsFolder ? `Global sessions are listed below; ${projectsFolder} is indexed here.` : "Global sessions are listed below; configure a projects folder in settings for faster picking." : mode === "name" ? "Enter starts the durable OMB host." : "Enter moves to the session name." })] })] }) }));
186
222
  }
187
- function SettingsModal({ settings, selected, columns, onToggle, onMove, onSetProjectsFolder, onSave, onClose }) {
223
+ function SettingsModal({ cwd, settings, selected, columns, onToggle, onMove, onSetProjectsFolder, onSave, onClose }) {
188
224
  const options = [
225
+ ["Project directory root", settings.projectsFolder || "not set"],
189
226
  ["Attach plain omb to latest live session", settings.autoAttachLatest],
190
227
  ["Enable mouse scrolling in OMB sessions", settings.mouseScrolling],
191
228
  ["Show stopped sessions in this view", settings.showStoppedSessions],
192
- ["Projects folder", settings.projectsFolder || "not set"],
193
229
  ];
194
230
  const [editingFolder, setEditingFolder] = useState(false);
195
231
  const [folderInput, setFolderInput] = useState(settings.projectsFolder);
232
+ const [folderSuggestions, setFolderSuggestions] = useState([]);
233
+ const [folderSuggestionSelected, setFolderSuggestionSelected] = useState(0);
234
+ useEffect(() => {
235
+ if (!editingFolder) {
236
+ setFolderSuggestions([]);
237
+ return;
238
+ }
239
+ let active = true;
240
+ void readDirectoryCompletions(folderInput, cwd).then(options => {
241
+ if (!active)
242
+ return;
243
+ setFolderSuggestions(options);
244
+ setFolderSuggestionSelected(0);
245
+ });
246
+ return () => { active = false; };
247
+ }, [cwd, editingFolder, folderInput]);
196
248
  useInput((input, key) => {
197
249
  if (editingFolder) {
198
250
  if (key.escape) {
199
251
  setFolderInput(settings.projectsFolder);
200
252
  setEditingFolder(false);
201
253
  }
254
+ else if (key.upArrow)
255
+ setFolderSuggestionSelected(value => Math.max(0, value - 1));
256
+ else if (key.downArrow)
257
+ setFolderSuggestionSelected(value => Math.min(Math.max(0, folderSuggestions.length - 1), value + 1));
258
+ else if (key.tab || key.rightArrow) {
259
+ const suggestion = folderSuggestions[folderSuggestionSelected];
260
+ if (suggestion)
261
+ setFolderInput(suggestion.path);
262
+ }
202
263
  else if (key.return) {
203
- onSetProjectsFolder(folderInput.trim());
264
+ onSetProjectsFolder(folderSuggestions[folderSuggestionSelected]?.path ?? folderInput.trim());
204
265
  setEditingFolder(false);
205
266
  }
206
267
  else if (key.backspace || (key.ctrl && input === "h"))
@@ -220,18 +281,18 @@ function SettingsModal({ settings, selected, columns, onToggle, onMove, onSetPro
220
281
  else if (input === "s")
221
282
  onSave();
222
283
  else if (input === " " || key.return) {
223
- if (selected === 3)
284
+ if (selected === 0)
224
285
  setEditingFolder(true);
225
286
  else
226
287
  onToggle();
227
288
  }
228
289
  });
229
- return (_jsxs(Modal, { title: "OMB SETTINGS", width: Math.min(70, Math.max(46, columns - 6)), columns: columns, children: [options.map(([label, enabled], index) => (_jsxs(Text, { color: index === selected ? "white" : undefined, backgroundColor: index === selected ? "blue" : undefined, children: [index === selected ? "›" : " ", " ", typeof enabled === "boolean" ? `[${enabled ? "on " : "off"}]` : enabled, " ", label] }, label))), selected === 3 && (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: editingFolder ? "cyan" : "gray", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "\u2315 " }), _jsx(Text, { children: editingFolder ? folderInput || "Type ~/projects or an absolute path" : settings.projectsFolder || "Press Enter to set a projects folder" })] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: editingFolder ? "Type path Enter apply Ctrl-U clear Esc cancel" : "Up/Down select Space/Enter edit s save Esc cancel" }), _jsx(Text, { color: "yellow", children: selected === 1 ? "Mouse mode applies to existing OMB sessions when saved." : selected === 3 ? "Child directories from this folder appear in the new-session picker." : "The hub always shows every Bizar session across projects." })] }));
290
+ return (_jsxs(Modal, { title: "OMB SETTINGS", width: Math.min(70, Math.max(46, columns - 6)), columns: columns, children: [options.map(([label, enabled], index) => (_jsxs(Text, { color: index === selected ? "white" : undefined, backgroundColor: index === selected ? "blue" : undefined, children: [index === selected ? "›" : " ", " ", typeof enabled === "boolean" ? `[${enabled ? "on " : "off"}]` : enabled, " ", label] }, label))), selected === 0 && (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: editingFolder ? "cyan" : "gray", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "\u2315 " }), _jsx(Text, { children: editingFolder ? folderInput || "Type ~/projects or an absolute path" : settings.projectsFolder || "Press Enter to set a projects folder" })] })), selected === 0 && editingFolder && folderSuggestions.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Autocomplete" }), folderSuggestions.slice(0, 5).map((suggestion, index) => (_jsxs(Text, { color: index === folderSuggestionSelected ? "white" : undefined, backgroundColor: index === folderSuggestionSelected ? "blue" : undefined, children: [index === folderSuggestionSelected ? "›" : " ", " ", suggestion.path] }, suggestion.path)))] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: editingFolder ? "Type path Enter apply Ctrl-U clear Esc cancel" : "Up/Down select Space/Enter edit s save Esc cancel" }), _jsx(Text, { color: "yellow", children: selected === 0 ? "Child directories from this folder appear in the new-session picker." : selected === 2 ? "Mouse mode applies to existing OMB sessions when saved." : "The hub always shows every Bizar session across projects." })] }));
230
291
  }
231
292
  function HelpModal({ columns, onClose }) {
232
293
  useInput((input, key) => { if (key.escape || key.return || input === "q")
233
294
  onClose(); });
234
- return (_jsxs(Modal, { title: "OMB AGENTS SHORTCUTS", width: Math.min(64, Math.max(42, columns - 6)), columns: columns, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "Enter" }), " attach to the selected session"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "n" }), " open the fuzzy directory session selector"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "e" }), " rename the selected session"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "d" }), " delete after confirmation"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "s" }), " open launcher settings"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "r" }), " refresh the session list"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "q / Esc" }), " leave this view"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Global view: every tmux session with the bizar- prefix is shown." }), _jsx(Text, { dimColor: true, children: "Press Enter or Esc to return." })] }));
295
+ return (_jsxs(Modal, { title: "OMB AGENTS SHORTCUTS", width: Math.min(64, Math.max(42, columns - 6)), columns: columns, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "Enter" }), " attach to the selected session"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "Tab" }), " focus workspaces; Enter immediately starts a named session there"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "n" }), " open the full new-session menu"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "Mouse" }), " click rows, scroll lists, and select a workspace"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "e" }), " rename the selected session"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "d" }), " delete after confirmation"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "s" }), " open launcher settings"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "r" }), " refresh the session list"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "q / Esc" }), " leave this view"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Global view: every tmux session with the bizar- prefix is shown." }), _jsx(Text, { dimColor: true, children: "Press Enter or Esc to return." })] }));
235
296
  }
236
297
  function DeleteModal({ session, columns, onConfirm, onClose }) {
237
298
  useInput((input, key) => {
@@ -242,24 +303,172 @@ function DeleteModal({ session, columns, onConfirm, onClose }) {
242
303
  });
243
304
  return (_jsxs(Modal, { title: "DELETE SESSION?", width: Math.min(64, Math.max(42, columns - 6)), columns: columns, children: [_jsxs(Text, { bold: true, children: [session.project, "/", session.id] }), _jsx(Text, { dimColor: true, children: short(session.cwd, 54) }), _jsx(Text, { children: " " }), _jsx(Text, { color: "yellow", children: "This stops OMP and removes the tmux session." }), _jsxs(Text, { children: ["Press ", _jsx(Text, { color: "red", bold: true, children: "y" }), " to delete, any other key to cancel."] })] }));
244
305
  }
245
- function AgentsApp({ cwd, env, sessions: initialSessions, settings: initialSettings, onAction }) {
306
+ function RenameModal({ session, columns, onConfirm, onClose }) {
307
+ const [value, setValue] = useState(session.id);
308
+ useInput((input, key) => {
309
+ if (key.escape)
310
+ onClose();
311
+ else if (key.return)
312
+ onConfirm(value.trim() || session.id);
313
+ else if (key.backspace || (key.ctrl && input === "h"))
314
+ setValue(current => current.slice(0, -1));
315
+ else if (key.ctrl && input === "u")
316
+ setValue("");
317
+ else if (input && !key.ctrl && !key.meta && input >= " ")
318
+ setValue(current => `${current}${input}`);
319
+ });
320
+ return (_jsxs(Modal, { title: "RENAME SESSION", width: Math.min(64, Math.max(42, columns - 6)), columns: columns, children: [_jsx(Text, { dimColor: true, children: short(session.cwd, 54) }), _jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { children: value || "Type a session name" })] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Enter save Ctrl-U clear Esc cancel" })] }));
321
+ }
322
+ /** Enable SGR mouse reporting while the hub owns the alternate screen. */
323
+ function useTerminalMouse(onMouse) {
324
+ const { stdin } = useStdin();
325
+ const { stdout } = useStdout();
326
+ const onMouseRef = useRef(onMouse);
327
+ useEffect(() => { onMouseRef.current = onMouse; }, [onMouse]);
328
+ useEffect(() => {
329
+ if (!stdin.isTTY)
330
+ return;
331
+ const enable = "\u001b[?1000h\u001b[?1006h";
332
+ const disable = "\u001b[?1000l\u001b[?1006l";
333
+ stdout.write(enable);
334
+ let buffer = "";
335
+ const pattern = /\u001b\[<(\d+);(\d+);(\d+)([mM])/g;
336
+ const onData = (chunk) => {
337
+ buffer += chunk.toString();
338
+ pattern.lastIndex = 0;
339
+ let consumed = 0;
340
+ let match;
341
+ while ((match = pattern.exec(buffer))) {
342
+ consumed = pattern.lastIndex;
343
+ onMouseRef.current({ button: Number(match[1]), x: Number(match[2]), y: Number(match[3]), release: match[4] === "m" });
344
+ }
345
+ const remainder = buffer.slice(consumed);
346
+ const escape = remainder.lastIndexOf("\u001b");
347
+ buffer = escape >= 0 ? remainder.slice(escape) : "";
348
+ if (buffer.length > 64)
349
+ buffer = buffer.slice(-64);
350
+ };
351
+ stdin.on("data", onData);
352
+ return () => {
353
+ stdin.off("data", onData);
354
+ stdout.write(disable);
355
+ };
356
+ }, [stdin, stdout]);
357
+ }
358
+ function sessionMouseIndex(sessions, selected, rows, y) {
359
+ const visibleCount = Math.max(4, rows - 7);
360
+ const start = Math.max(0, Math.min(Math.max(0, sessions.length - visibleCount), selected - Math.floor(visibleCount / 2)));
361
+ // The session panel starts below the header, panel border, title, and one
362
+ // spacer row. Keep this in sync with SessionList's compact layout.
363
+ let line = 11;
364
+ let previousWorkspace;
365
+ let previousState;
366
+ for (const [offset, session] of sessions.slice(start, start + visibleCount).entries()) {
367
+ if (session.cwd !== previousWorkspace) {
368
+ line += 1;
369
+ previousWorkspace = session.cwd;
370
+ previousState = undefined;
371
+ }
372
+ if (session.state !== previousState) {
373
+ line += 1;
374
+ previousState = session.state;
375
+ }
376
+ if (y === line || y === line + 1)
377
+ return start + offset;
378
+ line += 2;
379
+ }
380
+ return undefined;
381
+ }
382
+ function AgentsApp({ cwd, env, sessions: initialSessions, settings: initialSettings, initialSelectedName, onAction }) {
246
383
  const { columns, rows } = useWindowSize();
247
384
  const [allSessions, setAllSessions] = useState(initialSessions);
248
385
  const [settings, setSettings] = useState(initialSettings);
249
- const [selected, setSelected] = useState(0);
386
+ const [selectedName, setSelectedName] = useState(initialSelectedName ?? initialSessions[0]?.name);
387
+ const [selectedWorkspacePath, setSelectedWorkspacePath] = useState(initialSessions[0]?.cwd);
388
+ const [focus, setFocus] = useState("sessions");
389
+ const [newSessionDirectory, setNewSessionDirectory] = useState();
250
390
  const [overlay, setOverlay] = useState();
251
391
  const [overlaySelected, setOverlaySelected] = useState(0);
252
392
  const [draftSettings, setDraftSettings] = useState(initialSettings);
253
393
  const { exit } = useApp();
254
394
  const sessions = useMemo(() => visibleSessions(allSessions, settings), [allSessions, settings]);
395
+ const selected = Math.max(0, sessions.findIndex(session => session.name === selectedName));
255
396
  const selectedSession = sessions[selected];
397
+ const workspaces = useMemo(() => workspaceEntries(sessions), [sessions]);
398
+ const workspaceIndex = selectedWorkspacePath ? Math.max(0, workspaces.findIndex(workspace => workspace.path === selectedWorkspacePath) + 1) : 0;
256
399
  const refresh = async () => setAllSessions(await listOmbSessions(cwd, env));
257
400
  useEffect(() => {
258
401
  const timer = setInterval(() => { void refresh(); }, 3000);
259
402
  return () => clearInterval(timer);
260
403
  }, [cwd, env]);
261
- useEffect(() => { setSelected(value => Math.min(value, Math.max(0, sessions.length - 1))); }, [sessions.length]);
404
+ useEffect(() => {
405
+ if (sessions.length === 0) {
406
+ setSelectedName(undefined);
407
+ }
408
+ else if (!selectedName || !sessions.some(session => session.name === selectedName)) {
409
+ setSelectedName(sessions[0]?.name);
410
+ }
411
+ }, [sessions, selectedName]);
412
+ useEffect(() => {
413
+ if (selectedWorkspacePath && !workspaces.some(workspace => workspace.path === selectedWorkspacePath))
414
+ setSelectedWorkspacePath(workspaces[0]?.path);
415
+ }, [selectedWorkspacePath, workspaces]);
262
416
  const finish = (action) => { onAction(action); exit(); };
417
+ const openNewSession = (workspacePath) => {
418
+ setNewSessionDirectory(workspacePath);
419
+ setOverlay("new");
420
+ setFocus("sessions");
421
+ };
422
+ const launchWorkspaceSession = () => {
423
+ finish({ type: "new", projectCwd: selectedWorkspacePath ?? cwd, task: "", sessionId: "" });
424
+ };
425
+ const selectSession = (index) => {
426
+ const session = sessions[index];
427
+ if (!session)
428
+ return;
429
+ setSelectedName(session.name);
430
+ setSelectedWorkspacePath(workspacePath(session));
431
+ };
432
+ const selectWorkspace = (index) => {
433
+ if (index <= 0)
434
+ setSelectedWorkspacePath(undefined);
435
+ else
436
+ setSelectedWorkspacePath(workspaces[index - 1]?.path);
437
+ };
438
+ useTerminalMouse(event => {
439
+ if (overlay || event.release)
440
+ return;
441
+ if (event.button === 64 || event.button === 65) {
442
+ if (event.x <= Math.floor(columns * 0.28)) {
443
+ setFocus("workspaces");
444
+ selectWorkspace(Math.max(0, Math.min(workspaces.length, workspaceIndex + (event.button === 64 ? -1 : 1))));
445
+ }
446
+ else {
447
+ setFocus("sessions");
448
+ selectSession(Math.max(0, Math.min(Math.max(0, sessions.length - 1), selected + (event.button === 64 ? -1 : 1))));
449
+ }
450
+ return;
451
+ }
452
+ if (event.button !== 0)
453
+ return;
454
+ if (event.x <= Math.floor(columns * 0.28)) {
455
+ // Rail rows start at the ALL SESSIONS line and continue one row per
456
+ // workspace. The clamped index also makes clicks on the rail chrome
457
+ // useful instead of silently doing nothing.
458
+ const index = Math.max(0, Math.min(workspaces.length, event.y - 11));
459
+ setFocus("workspaces");
460
+ selectWorkspace(index);
461
+ return;
462
+ }
463
+ const index = sessionMouseIndex(sessions, selected, rows, event.y);
464
+ if (index === undefined)
465
+ return;
466
+ const session = sessions[index];
467
+ if (session?.name === selectedName)
468
+ finish({ type: "attach", session });
469
+ else
470
+ selectSession(index);
471
+ });
263
472
  useInput((input, key) => {
264
473
  if (overlay)
265
474
  return;
@@ -271,16 +480,30 @@ function AgentsApp({ cwd, env, sessions: initialSessions, settings: initialSetti
271
480
  finish({ type: "quit" });
272
481
  return;
273
482
  }
274
- if (key.upArrow || input === "k")
275
- setSelected(value => Math.max(0, value - 1));
276
- else if (key.downArrow || input === "j")
277
- setSelected(value => Math.min(Math.max(0, sessions.length - 1), value + 1));
483
+ if (key.tab) {
484
+ if (focus === "sessions") {
485
+ setFocus("workspaces");
486
+ setSelectedWorkspacePath(selectedSession ? workspacePath(selectedSession) : workspaces[0]?.path);
487
+ }
488
+ else
489
+ setFocus("sessions");
490
+ }
491
+ else if (focus === "workspaces" && (key.upArrow || input === "k"))
492
+ selectWorkspace(Math.max(0, workspaceIndex - 1));
493
+ else if (focus === "workspaces" && (key.downArrow || input === "j"))
494
+ selectWorkspace(Math.min(workspaces.length, workspaceIndex + 1));
495
+ else if (focus === "workspaces" && key.return)
496
+ launchWorkspaceSession();
497
+ else if (focus === "sessions" && (key.upArrow || input === "k"))
498
+ selectSession(Math.max(0, selected - 1));
499
+ else if (focus === "sessions" && (key.downArrow || input === "j"))
500
+ selectSession(Math.min(Math.max(0, sessions.length - 1), selected + 1));
278
501
  else if (key.return && selectedSession)
279
502
  finish({ type: "attach", session: selectedSession });
280
503
  else if (input === "n")
281
- setOverlay("new");
504
+ openNewSession();
282
505
  else if (input === "e" && selectedSession)
283
- finish({ type: "rename", session: selectedSession });
506
+ setOverlay("rename");
284
507
  else if (input === "d" && selectedSession) {
285
508
  setOverlay("delete");
286
509
  }
@@ -295,16 +518,16 @@ function AgentsApp({ cwd, env, sessions: initialSessions, settings: initialSetti
295
518
  setOverlay("settings");
296
519
  }
297
520
  });
298
- return (_jsxs(Box, { flexDirection: "column", width: "100%", minHeight: Math.max(12, rows - 1), paddingX: 1, children: [_jsx(Header, { cwd: cwd, sessions: sessions }), _jsxs(Box, { flexGrow: 1, marginTop: 1, gap: 1, children: [_jsx(WorkspaceRail, { sessions: sessions, selected: selectedSession }), _jsx(SessionList, { sessions: sessions, selected: selected, rows: rows, width: Math.max(34, Math.floor(columns * 0.42)) }), _jsx(Inspector, { session: selectedSession })] }), _jsx(Footer, {}), overlay === "new" && (_jsx(NewSessionModal, { cwd: cwd, sessions: allSessions, projectsFolder: settings.projectsFolder, selected: selectedSession, columns: columns, rows: rows, onSubmit: value => finish({ type: "new", ...value }), onClose: () => setOverlay(undefined) })), overlay === "help" && _jsx(HelpModal, { columns: columns, onClose: () => setOverlay(undefined) }), overlay === "delete" && selectedSession && _jsx(DeleteModal, { session: selectedSession, columns: columns, onConfirm: () => finish({ type: "delete", session: selectedSession }), onClose: () => setOverlay(undefined) }), overlay === "settings" && (_jsx(SettingsModal, { settings: draftSettings, selected: overlaySelected, onMove: setOverlaySelected, columns: columns, onToggle: () => {
299
- if (overlaySelected === 3)
521
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", minHeight: Math.max(12, rows - 1), paddingX: 1, children: [_jsx(Header, { cwd: cwd, sessions: sessions }), _jsxs(Box, { flexGrow: 1, marginTop: 1, gap: 1, children: [_jsx(WorkspaceRail, { sessions: sessions, selectedPath: selectedWorkspacePath, focused: focus === "workspaces" }), _jsx(SessionList, { sessions: sessions, selected: selected, rows: rows, width: Math.max(34, Math.floor(columns * 0.42)) }), _jsx(Inspector, { session: selectedSession })] }), _jsx(Footer, {}), overlay === "new" && (_jsx(NewSessionModal, { cwd: cwd, sessions: allSessions, projectsFolder: settings.projectsFolder, initialDirectory: newSessionDirectory, selected: selectedSession, columns: columns, rows: rows, onSubmit: value => finish({ type: "new", ...value }), onClose: () => setOverlay(undefined) })), overlay === "help" && _jsx(HelpModal, { columns: columns, onClose: () => setOverlay(undefined) }), overlay === "delete" && selectedSession && _jsx(DeleteModal, { session: selectedSession, columns: columns, onConfirm: () => finish({ type: "delete", session: selectedSession, nextSessionName: sessions[selected - 1]?.name ?? sessions[selected + 1]?.name }), onClose: () => setOverlay(undefined) }), overlay === "rename" && selectedSession && _jsx(RenameModal, { session: selectedSession, columns: columns, onConfirm: value => finish({ type: "rename", session: selectedSession, nextId: value }), onClose: () => setOverlay(undefined) }), overlay === "settings" && (_jsx(SettingsModal, { cwd: cwd, settings: draftSettings, selected: overlaySelected, onMove: setOverlaySelected, columns: columns, onToggle: () => {
522
+ if (overlaySelected === 0)
300
523
  return;
301
- const field = overlaySelected === 0 ? "autoAttachLatest" : overlaySelected === 1 ? "mouseScrolling" : "showStoppedSessions";
524
+ const field = overlaySelected === 1 ? "autoAttachLatest" : overlaySelected === 2 ? "mouseScrolling" : "showStoppedSessions";
302
525
  setDraftSettings(value => ({ ...value, [field]: !value[field] }));
303
526
  }, onSetProjectsFolder: value => setDraftSettings(current => ({ ...current, projectsFolder: value })), onSave: () => finish({ type: "settings", settings: draftSettings }), onClose: () => setOverlay(undefined) }))] }));
304
527
  }
305
- export async function runInkAgentsHub(cwd, env, sessions, settings) {
528
+ export async function runInkAgentsHub(cwd, env, sessions, settings, initialSelectedName) {
306
529
  let action = { type: "quit" };
307
- const app = render(_jsx(AgentsApp, { cwd: cwd, env: env, sessions: sessions, settings: settings, onAction: value => { action = value; } }), {
530
+ const app = render(_jsx(AgentsApp, { cwd: cwd, env: env, sessions: sessions, settings: settings, initialSelectedName: initialSelectedName, onAction: value => { action = value; } }), {
308
531
  alternateScreen: true,
309
532
  });
310
533
  await app.waitUntilExit();