@tt-a1i/hive 1.2.0 → 1.3.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,83 @@
2
2
 
3
3
  All notable user-facing changes will be documented in this file.
4
4
 
5
+ ## 1.3.0 - 2026-05-20
6
+
7
+ Installable Hive: turns the web shell into a real PWA so Chrome / Edge can
8
+ launch it from a dock icon, in its own window, without a visible browser
9
+ chrome.
10
+
11
+ - Adds a web app manifest with icons (192, 512, maskable 512, apple-touch 180),
12
+ a wide screenshot, and shortcuts for "Add Workspace" and "Try Demo" so
13
+ right-clicking the dock icon jumps straight to those flows.
14
+ - Installation is driven entirely by the browser's omnibox install icon
15
+ (Chrome / Edge / Brave); Hive deliberately does not add a redundant topbar
16
+ button.
17
+ - Ships a service worker (`/sw.js`) that caches the SPA shell + hashed asset
18
+ chunks + static icons / sounds / cli-icons, but never intercepts `/api/*`,
19
+ `/ws/*`, or non-GET requests — auth cookies and WebSockets keep their
20
+ native paths. Each release writes to its own cache bucket and older buckets
21
+ are kept so tabs still controlled by the previous SW can resolve their
22
+ lazy-imported chunks.
23
+ - Surfaces shell updates as a bottom-right toast (`Web UI updated — Reload to
24
+ activate`) instead of forcing a refresh. The Reload button stays disabled
25
+ while any terminal run is still working so updates never interrupt an
26
+ in-flight agent.
27
+ - Routes service-worker auto-reloads through the same silent reload helper used
28
+ elsewhere in the app, so browser updates do not trip the close-confirmation
29
+ guard.
30
+ - Replaces the workspace area with a dedicated `Hive runtime is not running`
31
+ page when the initial bootstrap fails. The page pings `/api/version` every
32
+ three seconds and reloads automatically once the daemon comes back; a manual
33
+ Retry button is offered alongside.
34
+ - Hardens the server: `/sw.js` is served with `Cache-Control: no-store` and the
35
+ manifest with `Cache-Control: max-age=0, must-revalidate`, so SW updates
36
+ propagate the next time the browser checks instead of waiting on a stale
37
+ HTTP cache.
38
+ - Notes for first-time installers: the SW activates after the first reload
39
+ following install. On separate ports (`hive --port 4011` vs `--port 3000`)
40
+ Chrome treats Hive as two distinct PWAs because the install scope is keyed
41
+ by origin. To fully remove a PWA install, use
42
+ `chrome://apps` → right-click the Hive tile → Remove.
43
+ - Always asks the browser to confirm before closing the tab or PWA window so
44
+ Cmd-W on an installed app never closes silently. Modern browsers gate the
45
+ prompt on prior page interaction — opening the window and immediately
46
+ pressing Cmd-W still closes cleanly by browser policy.
47
+ - Open Workspace dropdown now uses each app's brand color for its icon
48
+ instead of the previous monochrome white treatment, and visually separates
49
+ VS Code from VS Code Insiders so users can tell their installed targets
50
+ apart at a glance.
51
+ - Workspace avatars in the sidebar stay the same size when the user drags
52
+ the sidebar wider. Previously the wide layout used a 22px avatar while the
53
+ collapsed layout used 32px, so expanding the sidebar made the avatars
54
+ smaller; both modes now render at 32px.
55
+ - Drops IntelliJ IDEA, Windsurf, and iTerm2 from the Open Workspace dropdown.
56
+ IntelliJ users typically launch from JetBrains Toolbox rather than a folder
57
+ picker; Windsurf overlaps with the existing Cursor / VS Code entries;
58
+ iTerm2 overlaps with the built-in macOS Terminal entry. macOS now exposes
59
+ seven targets (VS Code, VS Code Insiders, Cursor, Finder, Terminal,
60
+ Ghostty, Zed); Windows / Linux expose five (VS Code, VS Code Insiders,
61
+ Cursor, File Explorer / File Manager, Zed). A stored preference for any
62
+ removed target silently falls back to the platform default at load time.
63
+ - Swaps the Zed, Ghostty, and Finder dropdown icons for the apps' official
64
+ brand marks (Finder uses the macOS app icon, Ghostty 96×96 / Zed 64×64
65
+ raster) so each entry reads as the real application rather than an
66
+ abstract glyph. Ghostty's mark renders inside a generous safe-zone so its
67
+ display size is bumped 20% via CSS scale to balance the row visually.
68
+ - Replaces the Worker detail modal and Workspace shell dialog with a docked,
69
+ resizable, VSCode-style terminal panel inside the right column (under the
70
+ team members pane). Worker tabs and shell tabs share the strip; clicking a
71
+ member card opens that worker as a tab; the panel hides when no tabs are
72
+ open. Closing a worker tab keeps the underlying PTY running — worker
73
+ lifecycle is owned by the card hover cluster. Tab list, active tab, and
74
+ panel height all persist (height globally, tabs + active per-workspace).
75
+ Cmd-W (Ctrl-W on Windows / Linux) closes the active tab; a "+" button in
76
+ the tab strip starts a new shell. Start failures and shell-start failures
77
+ now surface as toasts instead of inline modal/dialog banners.
78
+ - Moves "Save as template" into the role-instructions toolbar in the Add Member
79
+ flow, keeping template actions closer to the prompt editor instead of adding
80
+ another standalone control in the dialog body.
81
+
5
82
  ## 1.2.0 - 2026-05-18
6
83
 
7
84
  Opens the active workspace in your editor, terminal, or file manager from
package/README.en.md CHANGED
@@ -71,6 +71,39 @@ hive
71
71
  Open the printed local URL, usually `http://127.0.0.1:3000/`. Use
72
72
  `hive --port 4010` when you need a specific local port.
73
73
 
74
+ To upgrade in place:
75
+
76
+ ```bash
77
+ hive update
78
+ ```
79
+
80
+ `hive update` runs `npm install -g @tt-a1i/hive@latest` in place. Restart any
81
+ in-flight Hive process to pick up the new version. If you installed Hive with
82
+ pnpm or yarn, upgrade through the same package manager — otherwise the new
83
+ npm copy will shadow your existing install.
84
+
85
+ Install Hive as an app (optional):
86
+
87
+ Open `http://127.0.0.1:3000/` in Chrome, Edge, or Brave and click the install
88
+ icon at the right edge of the browser's omnibox. The PWA launches in its own
89
+ dock-anchored window without browser chrome and shows **Add Workspace** /
90
+ **Try Demo** shortcuts from the dock right-click menu. Firefox and Safari
91
+ currently don't implement the install-prompt protocol, so the omnibox icon
92
+ only appears in Chromium-based browsers.
93
+
94
+ The Hive daemon must still be running for the PWA to do anything; if the
95
+ runtime isn't reachable when you launch the app, you'll see a "Hive runtime
96
+ is not running" page that auto-reloads once `hive` is back on `127.0.0.1`.
97
+ The PWA install scope is keyed by origin, so `hive --port 4011` installs as
98
+ a separate app from `hive --port 3000`. To uninstall, visit `chrome://apps`,
99
+ right-click the Hive tile, and choose **Remove from Chrome…**.
100
+
101
+ Hive asks the browser to confirm before closing the tab or PWA window so an
102
+ accidental Cmd-W doesn't drop your session. Modern browsers gate that prompt
103
+ on prior page interaction — if you open the PWA and immediately press Cmd-W
104
+ without clicking or typing anywhere first, it still closes cleanly. That's a
105
+ browser policy, not a Hive bug.
106
+
74
107
  First-run flow:
75
108
 
76
109
  1. Create a workspace from a project folder.
package/README.md CHANGED
@@ -52,6 +52,22 @@ hive
52
52
 
53
53
  打开终端打印出来的本机地址,通常是 `http://127.0.0.1:3000/`。如果你想指定端口,可以用 `hive --port 4010`。
54
54
 
55
+ 升级到最新版本:
56
+
57
+ ```bash
58
+ hive update
59
+ ```
60
+
61
+ `hive update` 会在原位运行 `npm install -g @tt-a1i/hive@latest`,完事后重启 Hive 就能用上新版。如果当初是用 pnpm / yarn 装的 Hive,请用同一个包管理器升级,避免装出第二份。
62
+
63
+ 把 Hive 装为应用(可选):
64
+
65
+ 在 Chrome / Edge / Brave 里打开 `http://127.0.0.1:3000/`,点浏览器地址栏右侧的安装图标即可。装好后 Hive 会以独立窗口启动、有自己的 dock 图标,且 dock 右键菜单上会显示 **添加 Workspace** / **试用演示** 两个快捷入口。Firefox 和 Safari 暂未实现 PWA install-prompt 协议,浏览器地址栏的安装图标只在 Chromium 系浏览器里出现。
66
+
67
+ PWA 只是 UI 壳,Hive 后端仍需要在终端里跑着。如果启动 PWA 时后端没起,会看到 “Hive 后端未启动” 页面,等你跑起 `hive` 后会自动刷新。PWA 的 install scope 按 origin(含端口)划分,所以 `hive --port 4011` 跟 `hive --port 3000` 在浏览器看来是两个独立应用。卸载方法:浏览器地址栏访问 `chrome://apps`,右键 Hive 图标,选 **从 Chrome 中移除…**。
68
+
69
+ 关闭 PWA 窗口或 tab 时 Hive 会主动请求浏览器弹原生确认对话框,避免 Cmd+W 误关丢失会话。但现代浏览器要求你跟页面"交互过"(点击 / 滚动 / 输入)才会真的弹这个对话框——刚打开 PWA 立刻按 Cmd+W 仍会直接关闭,这是浏览器策略,不是 Hive 的 bug。
70
+
55
71
  首次使用流程:
56
72
 
57
73
  1. 选择一个项目目录作为 workspace。
@@ -0,0 +1,15 @@
1
+ export declare const HIVE_UPDATE_USAGE: string;
2
+ export interface RunUpdateResult {
3
+ exitCode: number;
4
+ spawnError?: Error;
5
+ }
6
+ export type RunUpdate = (command: string, args: readonly string[]) => Promise<RunUpdateResult>;
7
+ export declare const defaultRunUpdate: RunUpdate;
8
+ interface RunHiveUpdateOptions {
9
+ /** Inject a fake spawn for tests. */
10
+ runUpdate?: RunUpdate;
11
+ /** Override platform detection for tests. */
12
+ platform?: NodeJS.Platform;
13
+ }
14
+ export declare const runHiveUpdateCommand: (argv: string[], options?: RunHiveUpdateOptions) => Promise<number>;
15
+ export {};
@@ -0,0 +1,81 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { getNpmCommand, INSTALL_COMMAND_ARGS, INSTALL_COMMAND_DISPLAY, } from '../server/package-version.js';
3
+ export const HIVE_UPDATE_USAGE = [
4
+ 'Usage:',
5
+ ' hive update',
6
+ '',
7
+ `Runs \`${INSTALL_COMMAND_DISPLAY}\` to upgrade Hive in place.`,
8
+ 'Restart any running Hive process afterwards to pick up the new version.',
9
+ '',
10
+ 'Note: only npm-installed Hive can be upgraded this way. If you installed',
11
+ 'Hive via pnpm or yarn, upgrade through the same package manager instead;',
12
+ 'otherwise the npm copy will shadow your existing install.',
13
+ '',
14
+ 'Options:',
15
+ ' -h, --help Print this help.',
16
+ ].join('\n');
17
+ export const defaultRunUpdate = (command, args) => new Promise((resolve) => {
18
+ const child = spawn(command, [...args], { stdio: 'inherit' });
19
+ let resolved = false;
20
+ // Forward Ctrl+C / SIGTERM to the npm child so it can clean up rather
21
+ // than getting orphaned mid-install. The handlers are registered with
22
+ // `once` so they don't accumulate across invocations, and we also
23
+ // explicitly remove them when the child exits in case the user only
24
+ // sent one signal (Node would otherwise keep the handler alive).
25
+ const handleSignal = (signal) => () => {
26
+ child.kill(signal);
27
+ };
28
+ const handleSigint = handleSignal('SIGINT');
29
+ const handleSigterm = handleSignal('SIGTERM');
30
+ process.once('SIGINT', handleSigint);
31
+ process.once('SIGTERM', handleSigterm);
32
+ const finalize = (result) => {
33
+ if (resolved)
34
+ return;
35
+ resolved = true;
36
+ process.off('SIGINT', handleSigint);
37
+ process.off('SIGTERM', handleSigterm);
38
+ resolve(result);
39
+ };
40
+ child.on('error', (error) => {
41
+ finalize({ exitCode: 1, spawnError: error });
42
+ });
43
+ child.on('close', (code) => {
44
+ finalize({ exitCode: typeof code === 'number' ? code : 1 });
45
+ });
46
+ });
47
+ const printManualFallback = () => {
48
+ console.error(`You can run the upgrade manually: ${INSTALL_COMMAND_DISPLAY}`);
49
+ };
50
+ export const runHiveUpdateCommand = async (argv, options = {}) => {
51
+ if (argv.includes('--help') || argv.includes('-h')) {
52
+ console.log(HIVE_UPDATE_USAGE);
53
+ return 0;
54
+ }
55
+ // Reject unknown flags rather than silently ignoring them — keeps behavior
56
+ // consistent with how `parsePort` validates `hive` itself.
57
+ const extra = argv.find((arg) => arg !== '--help' && arg !== '-h');
58
+ if (extra !== undefined) {
59
+ console.error(`Unknown argument: ${extra}`);
60
+ console.error(HIVE_UPDATE_USAGE);
61
+ return 1;
62
+ }
63
+ const run = options.runUpdate ?? defaultRunUpdate;
64
+ const command = getNpmCommand(options.platform);
65
+ console.log(`Running: ${INSTALL_COMMAND_DISPLAY}`);
66
+ const result = await run(command, INSTALL_COMMAND_ARGS);
67
+ if (result.spawnError) {
68
+ console.error(`Failed to spawn npm: ${result.spawnError.message}`);
69
+ printManualFallback();
70
+ return 1;
71
+ }
72
+ if (result.exitCode === 0) {
73
+ console.log('Hive updated. Restart any running Hive process to pick up the new version.');
74
+ return 0;
75
+ }
76
+ console.error(`npm install exited with code ${result.exitCode}.`);
77
+ // Permission failures (EACCES on root-owned /usr/bin/npm) and other
78
+ // non-spawn errors leave the user with copy-paste recovery either way.
79
+ printManualFallback();
80
+ return result.exitCode;
81
+ };
@@ -9,14 +9,19 @@ import { createApp } from '../server/app.js';
9
9
  import { readPackageVersion } from '../server/package-version.js';
10
10
  import { createRuntimeStore } from '../server/runtime-store.js';
11
11
  import { createVersionService } from '../server/version-service.js';
12
+ import { runHiveUpdateCommand } from './hive-update.js';
12
13
  export const HIVE_USAGE = [
13
14
  'Usage:',
14
15
  ' hive [--port <port>]',
16
+ ' hive update',
15
17
  '',
16
18
  'Options:',
17
19
  ' --port <port> Bind the local runtime to a specific port (default: 3000).',
18
20
  ' -h, --help Print this help.',
19
21
  ' -v, --version Print the installed Hive version.',
22
+ '',
23
+ 'Commands:',
24
+ ' update Upgrade Hive in place via `npm install -g`.',
20
25
  ].join('\n');
21
26
  export const handleHiveInfoCommand = (argv) => {
22
27
  if (argv.includes('--help') || argv.includes('-h')) {
@@ -128,10 +133,21 @@ const isMainModule = process.argv[1]
128
133
  : false;
129
134
  if (isMainModule) {
130
135
  const argv = process.argv.slice(2);
131
- if (handleHiveInfoCommand(argv))
136
+ if (argv[0] === 'update') {
137
+ runHiveUpdateCommand(argv.slice(1))
138
+ .then((code) => process.exit(code))
139
+ .catch((error) => {
140
+ console.error(error);
141
+ process.exit(1);
142
+ });
143
+ }
144
+ else if (handleHiveInfoCommand(argv)) {
132
145
  process.exit(0);
133
- runHiveCommand(argv).catch((error) => {
134
- console.error(error);
135
- process.exit(1);
136
- });
146
+ }
147
+ else {
148
+ runHiveCommand(argv).catch((error) => {
149
+ console.error(error);
150
+ process.exit(1);
151
+ });
152
+ }
137
153
  }
@@ -27,7 +27,7 @@ export declare const createAgentRunStore: (db: Database) => {
27
27
  runId: string;
28
28
  agentId: string;
29
29
  pid: number | null;
30
- status: "starting" | "running" | "exited" | "error";
30
+ status: "error" | "starting" | "running" | "exited";
31
31
  exitCode: number | null;
32
32
  startedAt: number;
33
33
  endedAt: number | null;
@@ -54,6 +54,14 @@ const CONTENT_TYPES = {
54
54
  '.webp': 'image/webp',
55
55
  '.woff2': 'font/woff2',
56
56
  };
57
+ // PWA boot files must bypass HTTP caching: `sw.js` because the browser does its
58
+ // own byte-diff update check, and the manifest because Chrome consults it on
59
+ // every install/uninstall transition. Without these, SW updates can stall on a
60
+ // stale cached copy and the install prompt won't reflect a renamed app.
61
+ const PWA_BOOT_CACHE_CONTROL = {
62
+ '/manifest.webmanifest': 'max-age=0, must-revalidate',
63
+ '/sw.js': 'no-store',
64
+ };
57
65
  const sendStatic = async (response, staticDir, pathname, request) => {
58
66
  if (request.method !== 'GET' && request.method !== 'HEAD')
59
67
  return false;
@@ -63,6 +71,9 @@ const sendStatic = async (response, staticDir, pathname, request) => {
63
71
  try {
64
72
  const content = await readFile(filePath);
65
73
  response.setHeader('content-type', CONTENT_TYPES[extname(filePath)] ?? 'application/octet-stream');
74
+ const cacheControl = PWA_BOOT_CACHE_CONTROL[pathname];
75
+ if (cacheControl !== undefined)
76
+ response.setHeader('cache-control', cacheControl);
66
77
  response.statusCode = 200;
67
78
  response.end(request.method === 'HEAD' ? undefined : content);
68
79
  return true;
@@ -20,23 +20,10 @@ const macAttempts = (targetId, path) => {
20
20
  return [{ command: 'open', args: ['-a', 'Visual Studio Code - Insiders', path] }];
21
21
  case 'cursor':
22
22
  return [{ command: 'open', args: ['-a', 'Cursor', path] }];
23
- case 'windsurf':
24
- return [{ command: 'open', args: ['-a', 'Windsurf', path] }];
25
23
  case 'terminal':
26
24
  return [{ command: 'open', args: ['-a', 'Terminal', path] }];
27
- case 'iterm2':
28
- // Bundle name has always been `iTerm.app` even after the iTerm2 rename;
29
- // the `iTerm2` fallback in earlier ports is cargo-cult.
30
- return [{ command: 'open', args: ['-a', 'iTerm', path] }];
31
25
  case 'ghostty':
32
26
  return [{ command: 'open', args: ['-a', 'Ghostty', path] }];
33
- case 'intellijidea':
34
- // 2025.3 unified the editions, but installs predating that still ship
35
- // `IntelliJ IDEA CE.app` — retry once on the CE bundle.
36
- return [
37
- { command: 'open', args: ['-a', 'IntelliJ IDEA', path] },
38
- { command: 'open', args: ['-a', 'IntelliJ IDEA CE', path] },
39
- ];
40
27
  case 'zed':
41
28
  return [{ command: 'open', args: ['-a', 'Zed', path] }];
42
29
  }
@@ -51,8 +38,6 @@ const linuxAttempts = (targetId, path) => {
51
38
  return [{ command: 'code-insiders', args: [path] }];
52
39
  case 'cursor':
53
40
  return [{ command: 'cursor', args: [path] }];
54
- case 'windsurf':
55
- return [{ command: 'windsurf', args: [path] }];
56
41
  case 'zed':
57
42
  return [{ command: 'zed', args: [path] }];
58
43
  default:
@@ -69,8 +54,6 @@ const windowsAttempts = (targetId, path) => {
69
54
  return [{ command: 'code-insiders', args: [path] }];
70
55
  case 'cursor':
71
56
  return [{ command: 'cursor', args: [path] }];
72
- case 'windsurf':
73
- return [{ command: 'windsurf', args: [path] }];
74
57
  case 'zed':
75
58
  return [{ command: 'zed', args: [path] }];
76
59
  default:
@@ -1,2 +1,17 @@
1
1
  export declare const PACKAGE_NAME = "@tt-a1i/hive";
2
+ /**
3
+ * Canonical argv for the upgrade command. Sharing one source between the
4
+ * server's install hint (`version-service.ts`) and the CLI upgrade path
5
+ * (`hive-update.ts`) keeps the two from drifting if the package name ever
6
+ * moves.
7
+ */
8
+ export declare const INSTALL_COMMAND_ARGS: readonly ["install", "-g", "@tt-a1i/hive@latest"];
9
+ export declare const INSTALL_COMMAND_DISPLAY: string;
10
+ /**
11
+ * Windows ships npm as `npm.cmd` (a batch shim); Node's `child_process.spawn`
12
+ * will not resolve `.cmd` without `shell: true` or an explicit suffix, so the
13
+ * default `'npm'` produces ENOENT on Windows. Use this helper any time you
14
+ * spawn npm directly.
15
+ */
16
+ export declare const getNpmCommand: (platform?: NodeJS.Platform) => string;
2
17
  export declare const readPackageVersion: () => string;
@@ -2,6 +2,21 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  export const PACKAGE_NAME = '@tt-a1i/hive';
5
+ /**
6
+ * Canonical argv for the upgrade command. Sharing one source between the
7
+ * server's install hint (`version-service.ts`) and the CLI upgrade path
8
+ * (`hive-update.ts`) keeps the two from drifting if the package name ever
9
+ * moves.
10
+ */
11
+ export const INSTALL_COMMAND_ARGS = ['install', '-g', `${PACKAGE_NAME}@latest`];
12
+ export const INSTALL_COMMAND_DISPLAY = `npm ${INSTALL_COMMAND_ARGS.join(' ')}`;
13
+ /**
14
+ * Windows ships npm as `npm.cmd` (a batch shim); Node's `child_process.spawn`
15
+ * will not resolve `.cmd` without `shell: true` or an explicit suffix, so the
16
+ * default `'npm'` produces ENOENT on Windows. Use this helper any time you
17
+ * spawn npm directly.
18
+ */
19
+ export const getNpmCommand = (platform = process.platform) => platform === 'win32' ? 'npm.cmd' : 'npm';
5
20
  export const readPackageVersion = () => {
6
21
  let dir = dirname(fileURLToPath(import.meta.url));
7
22
  for (let depth = 0; depth < 8; depth += 1) {
@@ -27,7 +27,10 @@ export const compareVersions = (left, right) => {
27
27
  };
28
28
  const buildVersionInfo = (currentVersion, latestVersion) => ({
29
29
  current_version: currentVersion,
30
- install_hint: `npm install -g ${PACKAGE_NAME}@latest`,
30
+ // Show `hive update` as the primary upgrade path now that the subcommand
31
+ // exists; it prints the npm fallback if its own spawn fails, so users
32
+ // running on Windows / Linux without a working npm in PATH still recover.
33
+ install_hint: 'hive update',
31
34
  latest_version: latestVersion,
32
35
  package_name: PACKAGE_NAME,
33
36
  release_url: `https://www.npmjs.com/package/${PACKAGE_NAME}/v/${latestVersion}`,
@@ -1,5 +1,7 @@
1
1
  import { basename } from 'node:path';
2
2
  const WORKSPACE_SHELL_SUFFIX = ':shell';
3
+ const WORKSPACE_SHELL_LABEL = 'Shell';
4
+ const EXITED_SHELL_RETENTION_MS = 5000;
3
5
  export const getWorkspaceShellAgentId = (workspaceId) => `${workspaceId}${WORKSPACE_SHELL_SUFFIX}`;
4
6
  export const isWorkspaceShellAgentId = (agentId) => agentId.endsWith(WORKSPACE_SHELL_SUFFIX);
5
7
  const shouldUseLoginShell = (command) => {
@@ -20,10 +22,10 @@ export const resolveWorkspaceShellLaunch = (env = process.env, platform = proces
20
22
  };
21
23
  export const createWorkspaceShellRuntime = (agentManager) => {
22
24
  const labelsByRunId = new Map();
23
- const nextShellNumberByWorkspaceId = new Map();
24
25
  const workspaceIdsByRunId = new Map();
25
26
  const runIdsByWorkspaceId = new Map();
26
27
  const startedAtByRunId = new Map();
28
+ const exitCleanupTimersByRunId = new Map();
27
29
  const requireManager = () => {
28
30
  if (!agentManager)
29
31
  throw new Error('Agent manager is required for workspace shell terminals');
@@ -43,6 +45,10 @@ export const createWorkspaceShellRuntime = (agentManager) => {
43
45
  }
44
46
  };
45
47
  const detachRun = (runId) => {
48
+ const exitCleanupTimer = exitCleanupTimersByRunId.get(runId);
49
+ if (exitCleanupTimer)
50
+ clearTimeout(exitCleanupTimer);
51
+ exitCleanupTimersByRunId.delete(runId);
46
52
  const workspaceId = workspaceIdsByRunId.get(runId);
47
53
  if (workspaceId) {
48
54
  const retained = (runIdsByWorkspaceId.get(workspaceId) ?? []).filter((id) => id !== runId);
@@ -61,11 +67,27 @@ export const createWorkspaceShellRuntime = (agentManager) => {
61
67
  startedAtByRunId.set(runId, startedAt);
62
68
  runIdsByWorkspaceId.set(workspaceId, [...(runIdsByWorkspaceId.get(workspaceId) ?? []), runId]);
63
69
  };
64
- const nextLabel = (workspaceId) => {
65
- const next = nextShellNumberByWorkspaceId.get(workspaceId) ?? 1;
66
- nextShellNumberByWorkspaceId.set(workspaceId, next + 1);
67
- return `Shell ${next}`;
70
+ const forgetShellRun = (runId) => {
71
+ detachRun(runId);
72
+ try {
73
+ requireManager().removeRun(runId);
74
+ }
75
+ catch {
76
+ // The PTY manager may have already dropped the run.
77
+ }
78
+ };
79
+ const handleShellExit = (runId) => {
80
+ if (!hasRun(runId) || exitCleanupTimersByRunId.has(runId))
81
+ return;
82
+ const timer = setTimeout(() => {
83
+ exitCleanupTimersByRunId.delete(runId);
84
+ if (hasRun(runId))
85
+ forgetShellRun(runId);
86
+ }, EXITED_SHELL_RETENTION_MS);
87
+ timer.unref?.();
88
+ exitCleanupTimersByRunId.set(runId, timer);
68
89
  };
90
+ const isListedRun = (run) => run.status === 'starting' || run.status === 'running';
69
91
  const stopPtyRun = (runId) => {
70
92
  requireManager().stopRun(runId);
71
93
  };
@@ -92,7 +114,9 @@ export const createWorkspaceShellRuntime = (agentManager) => {
92
114
  workspaceIdsByRunId.clear();
93
115
  startedAtByRunId.clear();
94
116
  labelsByRunId.clear();
95
- nextShellNumberByWorkspaceId.clear();
117
+ for (const timer of exitCleanupTimersByRunId.values())
118
+ clearTimeout(timer);
119
+ exitCleanupTimersByRunId.clear();
96
120
  },
97
121
  closeRun(workspaceId, runId) {
98
122
  if (workspaceIdsByRunId.get(runId) !== workspaceId)
@@ -110,7 +134,6 @@ export const createWorkspaceShellRuntime = (agentManager) => {
110
134
  }
111
135
  }
112
136
  runIdsByWorkspaceId.delete(workspaceId);
113
- nextShellNumberByWorkspaceId.delete(workspaceId);
114
137
  },
115
138
  getLiveRun(runId) {
116
139
  if (!hasRun(runId))
@@ -122,6 +145,8 @@ export const createWorkspaceShellRuntime = (agentManager) => {
122
145
  return (runIdsByWorkspaceId.get(workspaceId) ?? []).flatMap((runId) => {
123
146
  try {
124
147
  const run = toLiveRun(runId);
148
+ if (!isListedRun(run))
149
+ return [];
125
150
  return [
126
151
  {
127
152
  agent_id: getWorkspaceShellAgentId(workspaceId),
@@ -163,8 +188,9 @@ export const createWorkspaceShellRuntime = (agentManager) => {
163
188
  TERM: 'xterm-256color',
164
189
  TERM_PROGRAM: 'hive-shell',
165
190
  },
191
+ onExit: ({ runId }) => handleShellExit(runId),
166
192
  });
167
- attachRun(workspace.id, run.runId, nextLabel(workspace.id), startedAt);
193
+ attachRun(workspace.id, run.runId, WORKSPACE_SHELL_LABEL, startedAt);
168
194
  return { ...run, startedAt };
169
195
  },
170
196
  stopRun(runId) {
@@ -4,7 +4,7 @@
4
4
  * and the web client (button + preference store in `web/src/workspace/open-targets.ts`)
5
5
  * pull the union and platform whitelist from here so they cannot drift.
6
6
  */
7
- export type OpenTargetId = 'vscode' | 'vscode-insiders' | 'cursor' | 'windsurf' | 'finder' | 'terminal' | 'iterm2' | 'ghostty' | 'intellijidea' | 'zed';
7
+ export type OpenTargetId = 'vscode' | 'vscode-insiders' | 'cursor' | 'finder' | 'terminal' | 'ghostty' | 'zed';
8
8
  export type OpenTargetPlatform = 'mac' | 'windows' | 'linux' | 'other';
9
9
  export declare const OPEN_TARGET_IDS_BY_PLATFORM: Record<OpenTargetPlatform, readonly OpenTargetId[]>;
10
10
  export declare const isOpenTargetId: (value: unknown) => value is OpenTargetId;
@@ -6,23 +6,16 @@
6
6
  */
7
7
  // Note: there is no `cursor-insiders` here. Cursor stopped shipping a separate
8
8
  // Nightly bundle / `cursor-nightly` binary in March 2024; the pre-release
9
- // channel is now an in-app toggle on the regular Cursor.app, so an "Insiders"
10
- // menu entry would 100% fail with `app-not-installed` on every platform.
9
+ // channel is now an in-app toggle on the regular Cursor.app.
10
+ //
11
+ // IntelliJ IDEA, Windsurf, and iTerm2 were intentionally dropped after 1.3.0:
12
+ // IntelliJ users typically launch from JetBrains Toolbox rather than a folder
13
+ // picker; Windsurf overlaps with Cursor/VS Code; iTerm2 overlaps with the
14
+ // built-in macOS Terminal entry.
11
15
  export const OPEN_TARGET_IDS_BY_PLATFORM = {
12
- mac: [
13
- 'vscode',
14
- 'vscode-insiders',
15
- 'cursor',
16
- 'windsurf',
17
- 'finder',
18
- 'terminal',
19
- 'iterm2',
20
- 'ghostty',
21
- 'intellijidea',
22
- 'zed',
23
- ],
24
- windows: ['vscode', 'vscode-insiders', 'cursor', 'windsurf', 'finder', 'zed'],
25
- linux: ['vscode', 'vscode-insiders', 'cursor', 'windsurf', 'finder', 'zed'],
16
+ mac: ['vscode', 'vscode-insiders', 'cursor', 'finder', 'terminal', 'ghostty', 'zed'],
17
+ windows: ['vscode', 'vscode-insiders', 'cursor', 'finder', 'zed'],
18
+ linux: ['vscode', 'vscode-insiders', 'cursor', 'finder', 'zed'],
26
19
  other: ['vscode', 'vscode-insiders', 'finder'],
27
20
  };
28
21
  const ALL_TARGET_IDS = new Set(OPEN_TARGET_IDS_BY_PLATFORM.mac);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tt-a1i/hive",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Browser-native hive-mind for CLI coding agents — Claude Code, Codex, Gemini, and OpenCode collaborate as real PTY processes via a team protocol.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.30.3",
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/tt-a1i/hive.git"
20
+ "url": "git+https://github.com/tt-a1i/hive-private.git"
21
21
  },
22
22
  "bugs": {
23
23
  "url": "https://github.com/tt-a1i/hive/issues"
@@ -60,7 +60,7 @@
60
60
  "postinstall": "node scripts/fix-runtime-artifacts.mjs",
61
61
  "release:dry": "pnpm check && pnpm build && pnpm test && pnpm pack:check && pnpm pack:smoke",
62
62
  "test": "vitest run",
63
- "test:windows": "vitest run tests/unit/agent-command-resolver.test.ts tests/unit/session-capture-multi-cli.test.ts tests/unit/claude-session-support.test.ts tests/unit/worker-name-generator.test.ts tests/unit/open-target-commands.test.ts tests/server/fs-pick-folder.test.ts tests/server/fs-browse.test.ts tests/server/schema-version.test.ts tests/server/runtime-rehydration.test.ts tests/server/open-workspace-route.test.ts tests/web/workspace-picker.test.tsx tests/web/confirm-dialog.test.tsx tests/web/toast.test.tsx tests/web/open-workspace-button.test.tsx --no-file-parallelism --maxWorkers=1 --testTimeout=30000 --hookTimeout=30000",
63
+ "test:windows": "vitest run tests/unit/agent-command-resolver.test.ts tests/unit/session-capture-multi-cli.test.ts tests/unit/claude-session-support.test.ts tests/unit/worker-name-generator.test.ts tests/unit/open-target-commands.test.ts tests/unit/sw-template-substitution.test.ts tests/unit/build-sw-plugin.test.ts tests/server/fs-pick-folder.test.ts tests/server/fs-browse.test.ts tests/server/schema-version.test.ts tests/server/runtime-rehydration.test.ts tests/server/open-workspace-route.test.ts tests/server/static-pwa.test.ts tests/web/workspace-picker.test.tsx tests/web/confirm-dialog.test.tsx tests/web/toast.test.tsx tests/web/open-workspace-button.test.tsx tests/web/register-service-worker.test.ts tests/web/use-shortcut-action.test.ts tests/web/update-available-toast.test.tsx tests/web/runtime-offline-page.test.tsx tests/web/use-terminal-panel-height.test.ts tests/web/use-terminal-panel-tabs.test.ts tests/web/terminal-tabs.test.tsx tests/web/terminal-bottom-panel.test.tsx --no-file-parallelism --maxWorkers=1 --testTimeout=60000 --hookTimeout=60000",
64
64
  "test:watch": "vitest"
65
65
  },
66
66
  "dependencies": {