janissary 0.5.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.
Files changed (268) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +28 -0
  3. package/agent-names.json +52 -0
  4. package/bin/janus.mjs +27 -0
  5. package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
  6. package/chrome-extension/content-script.js +62 -0
  7. package/chrome-extension/manifest.json +25 -0
  8. package/chrome-extension/rules.json +16 -0
  9. package/dist/acp/index.js +93 -0
  10. package/dist/acp/loop.js +75 -0
  11. package/dist/acp/manager.js +126 -0
  12. package/dist/acp/rate-limit.js +8 -0
  13. package/dist/acp/runner.js +18 -0
  14. package/dist/acp/tools.js +27 -0
  15. package/dist/acp-loop.js +75 -0
  16. package/dist/acp-manager.js +109 -0
  17. package/dist/acp-runner.js +18 -0
  18. package/dist/acp.js +91 -0
  19. package/dist/agent/commands.js +29 -0
  20. package/dist/agent/communication-manager.js +71 -0
  21. package/dist/agent/state.js +58 -0
  22. package/dist/agent-commands.js +29 -0
  23. package/dist/agent-communication-manager.js +71 -0
  24. package/dist/agent-state.js +58 -0
  25. package/dist/app-themes.js +13 -0
  26. package/dist/browser/command.js +104 -0
  27. package/dist/browser/index.js +124 -0
  28. package/dist/browser/tab-helpers.js +51 -0
  29. package/dist/browser/tab.js +99 -0
  30. package/dist/browser-command.js +104 -0
  31. package/dist/browser-tab-helpers.js +51 -0
  32. package/dist/browser-tab.js +99 -0
  33. package/dist/browser.js +124 -0
  34. package/dist/buffer.js +42 -0
  35. package/dist/bus.js +52 -0
  36. package/dist/capture/manager.js +48 -0
  37. package/dist/capture/router.js +18 -0
  38. package/dist/capture-manager.js +48 -0
  39. package/dist/capture-router.js +20 -0
  40. package/dist/chrome-extension-loader.js +62 -0
  41. package/dist/cli-args.js +80 -0
  42. package/dist/command-manager.js +126 -0
  43. package/dist/command-router.js +14 -0
  44. package/dist/commands/acp-reset.js +13 -0
  45. package/dist/commands/acp.js +5 -0
  46. package/dist/commands/agent.js +5 -0
  47. package/dist/commands/broadcast.js +22 -0
  48. package/dist/commands/browser.js +5 -0
  49. package/dist/commands/clear.js +5 -0
  50. package/dist/commands/close.js +42 -0
  51. package/dist/commands/connection.js +5 -0
  52. package/dist/commands/db.js +5 -0
  53. package/dist/commands/edit.js +18 -0
  54. package/dist/commands/files.js +13 -0
  55. package/dist/commands/hist.js +7 -0
  56. package/dist/commands/index.js +62 -0
  57. package/dist/commands/monitor.js +61 -0
  58. package/dist/commands/msg.js +17 -0
  59. package/dist/commands/next.js +7 -0
  60. package/dist/commands/notifications.js +16 -0
  61. package/dist/commands/notify.js +20 -0
  62. package/dist/commands/open.js +41 -0
  63. package/dist/commands/profile.js +5 -0
  64. package/dist/commands/queue.js +37 -0
  65. package/dist/commands/quit.js +6 -0
  66. package/dist/commands/rename.js +12 -0
  67. package/dist/commands/resolve-target.js +12 -0
  68. package/dist/commands/schedule.js +74 -0
  69. package/dist/commands/search.js +26 -0
  70. package/dist/commands/send.js +51 -0
  71. package/dist/commands/state.js +9 -0
  72. package/dist/commands/syntax.js +36 -0
  73. package/dist/commands/tasks.js +7 -0
  74. package/dist/commands/theme.js +41 -0
  75. package/dist/commands/types.js +1 -0
  76. package/dist/commands.js +70 -0
  77. package/dist/completion/browser.js +24 -0
  78. package/dist/completion/fs.js +26 -0
  79. package/dist/completion/handlers.js +81 -0
  80. package/dist/completion/helpers.js +43 -0
  81. package/dist/completion/index.js +43 -0
  82. package/dist/completion-browser.js +24 -0
  83. package/dist/completion-fs.js +26 -0
  84. package/dist/completion-handlers.js +69 -0
  85. package/dist/completion-helpers.js +43 -0
  86. package/dist/completion.js +42 -0
  87. package/dist/config.js +66 -0
  88. package/dist/connection/close.js +41 -0
  89. package/dist/connection/manager.js +93 -0
  90. package/dist/connection/parsing.js +28 -0
  91. package/dist/connection-close.js +41 -0
  92. package/dist/connection-manager.js +93 -0
  93. package/dist/connection-parsing.js +28 -0
  94. package/dist/connections.js +75 -0
  95. package/dist/controller-events.js +34 -0
  96. package/dist/controller-file-tree.js +36 -0
  97. package/dist/controller.js +201 -0
  98. package/dist/database/index.js +83 -0
  99. package/dist/database/manager.js +67 -0
  100. package/dist/database/parsing.js +54 -0
  101. package/dist/database/query.js +41 -0
  102. package/dist/database-manager.js +67 -0
  103. package/dist/database-parsing.js +54 -0
  104. package/dist/database-query.js +41 -0
  105. package/dist/database.js +83 -0
  106. package/dist/datetime.js +15 -0
  107. package/dist/editor/save.js +27 -0
  108. package/dist/editor/sync.js +10 -0
  109. package/dist/editor/watch-manager.js +84 -0
  110. package/dist/editor-save.js +24 -0
  111. package/dist/editor-watch-manager.js +84 -0
  112. package/dist/file-tree-args.js +36 -0
  113. package/dist/file-tree-git-refresh.js +33 -0
  114. package/dist/file-tree-manager.js +252 -0
  115. package/dist/file-tree-moves.js +26 -0
  116. package/dist/file-tree-open.js +56 -0
  117. package/dist/file-tree-rebuild.js +22 -0
  118. package/dist/file-tree-watch.js +27 -0
  119. package/dist/file-tree.js +82 -0
  120. package/dist/git-status.js +64 -0
  121. package/dist/github-token.js +19 -0
  122. package/dist/global-history.js +48 -0
  123. package/dist/harness/auto-approve.js +79 -0
  124. package/dist/harness/busy-status.js +91 -0
  125. package/dist/harness/capture-file.js +28 -0
  126. package/dist/harness/index.js +87 -0
  127. package/dist/harness/manager.js +182 -0
  128. package/dist/harness/models.js +8 -0
  129. package/dist/harness/recorder.js +89 -0
  130. package/dist/harness/recording-file.js +25 -0
  131. package/dist/harness/screen.js +79 -0
  132. package/dist/harness-capture-file.js +28 -0
  133. package/dist/harness-manager.js +112 -0
  134. package/dist/harness-models.js +8 -0
  135. package/dist/harness-screen.js +72 -0
  136. package/dist/harness.js +54 -0
  137. package/dist/index.js +163 -0
  138. package/dist/instance-lock.js +34 -0
  139. package/dist/interactive.js +33 -0
  140. package/dist/main.js +203 -0
  141. package/dist/managers.js +1 -0
  142. package/dist/message-handler.js +134 -0
  143. package/dist/messaging.js +47 -0
  144. package/dist/monitor/acp.js +24 -0
  145. package/dist/monitor/ask.js +31 -0
  146. package/dist/monitor/context.js +27 -0
  147. package/dist/monitor/editor-feed.js +49 -0
  148. package/dist/monitor/feed-diff.js +22 -0
  149. package/dist/monitor/feeds.js +19 -0
  150. package/dist/monitor/framing.js +29 -0
  151. package/dist/monitor/harness-feed.js +21 -0
  152. package/dist/monitor/info.js +29 -0
  153. package/dist/monitor/manager.js +221 -0
  154. package/dist/monitor/page-feed.js +21 -0
  155. package/dist/monitor/parsing.js +82 -0
  156. package/dist/monitor/reply.js +11 -0
  157. package/dist/monitor/session.js +36 -0
  158. package/dist/monitor/stop.js +33 -0
  159. package/dist/monitor/suggestion.js +12 -0
  160. package/dist/monitor/targets.js +69 -0
  161. package/dist/monitor/window.js +109 -0
  162. package/dist/monitor-acp.js +22 -0
  163. package/dist/monitor-info.js +18 -0
  164. package/dist/monitor-manager.js +233 -0
  165. package/dist/monitor-parsing.js +68 -0
  166. package/dist/monitor-session.js +19 -0
  167. package/dist/monitor-targets.js +37 -0
  168. package/dist/monitor-window.js +81 -0
  169. package/dist/notifications-tab.js +32 -0
  170. package/dist/notifications.js +87 -0
  171. package/dist/open-file-manager.js +100 -0
  172. package/dist/openers/editor.js +41 -0
  173. package/dist/openers/external-viewer.js +11 -0
  174. package/dist/openers/image.js +27 -0
  175. package/dist/openers/index.js +16 -0
  176. package/dist/openers/markdown.js +20 -0
  177. package/dist/openers/os-open.js +24 -0
  178. package/dist/openers/page.js +51 -0
  179. package/dist/openers/size.js +13 -0
  180. package/dist/openers/types.js +1 -0
  181. package/dist/page/sync.js +10 -0
  182. package/dist/paths.js +45 -0
  183. package/dist/persona-parsing.js +49 -0
  184. package/dist/personas.js +44 -0
  185. package/dist/profile/agent-opener.js +154 -0
  186. package/dist/profile/files.js +17 -0
  187. package/dist/profile/manager.js +72 -0
  188. package/dist/profile/monitors.js +23 -0
  189. package/dist/profile/notifications.js +11 -0
  190. package/dist/profile-agent-opener.js +136 -0
  191. package/dist/profile-manager.js +72 -0
  192. package/dist/profiles.js +143 -0
  193. package/dist/protocol.js +1 -0
  194. package/dist/pseudoterminal-manager.js +106 -0
  195. package/dist/pty.js +41 -0
  196. package/dist/recognizers/acp.js +38 -0
  197. package/dist/recognizers/analyze.js +49 -0
  198. package/dist/recognizers/bash.js +51 -0
  199. package/dist/recognizers/db.js +26 -0
  200. package/dist/recognizers/index.js +1 -0
  201. package/dist/recognizers/lexicon.js +11 -0
  202. package/dist/recognizers/types.js +5 -0
  203. package/dist/resolve.js +39 -0
  204. package/dist/route-choice.js +11 -0
  205. package/dist/sandbox/index.js +217 -0
  206. package/dist/sandbox/paths.js +178 -0
  207. package/dist/sandbox/profile.js +161 -0
  208. package/dist/sandbox-paths.js +159 -0
  209. package/dist/sandbox-profile.js +161 -0
  210. package/dist/sandbox.js +217 -0
  211. package/dist/schedule/display.js +19 -0
  212. package/dist/schedule/helpers.js +76 -0
  213. package/dist/schedule/index.js +76 -0
  214. package/dist/schedule/manager.js +101 -0
  215. package/dist/schedule/parsing.js +63 -0
  216. package/dist/schedule/time.js +34 -0
  217. package/dist/schedule-display.js +19 -0
  218. package/dist/schedule-helpers.js +76 -0
  219. package/dist/schedule-manager.js +98 -0
  220. package/dist/schedule-parsing.js +63 -0
  221. package/dist/schedule-time.js +34 -0
  222. package/dist/schedule.js +76 -0
  223. package/dist/search-matches.js +40 -0
  224. package/dist/security.js +44 -0
  225. package/dist/shell-manager.js +139 -0
  226. package/dist/shell.js +59 -0
  227. package/dist/ssh-manager.js +42 -0
  228. package/dist/ssh.js +39 -0
  229. package/dist/startup-errors.js +21 -0
  230. package/dist/state-event.js +14 -0
  231. package/dist/state-format.js +38 -0
  232. package/dist/syntax-themes.js +14 -0
  233. package/dist/tab/agent-state.js +19 -0
  234. package/dist/tab/cleanup.js +33 -0
  235. package/dist/tab/colors.js +43 -0
  236. package/dist/tab/creators.js +95 -0
  237. package/dist/tab/dock.js +35 -0
  238. package/dist/tab/focus-history.js +41 -0
  239. package/dist/tab/formatting-handlers.js +77 -0
  240. package/dist/tab/formatting.js +21 -0
  241. package/dist/tab/history.js +14 -0
  242. package/dist/tab/index.js +65 -0
  243. package/dist/tab/manager.js +329 -0
  244. package/dist/tab/openers.js +35 -0
  245. package/dist/tab/queue.js +33 -0
  246. package/dist/tab/rehydrate.js +15 -0
  247. package/dist/tab/reorder.js +15 -0
  248. package/dist/tab/transcript.js +15 -0
  249. package/dist/tab/utils.js +55 -0
  250. package/dist/tab/view.js +42 -0
  251. package/dist/tab-cleanup.js +27 -0
  252. package/dist/tab-colors.js +43 -0
  253. package/dist/tab-creators.js +85 -0
  254. package/dist/tab-formatting-handlers.js +77 -0
  255. package/dist/tab-formatting.js +21 -0
  256. package/dist/tab-manager.js +418 -0
  257. package/dist/tab-utils.js +55 -0
  258. package/dist/tab.js +57 -0
  259. package/dist/tasks.js +32 -0
  260. package/dist/transcript/logger.js +36 -0
  261. package/dist/transcript/store.js +66 -0
  262. package/dist/types.js +6 -0
  263. package/dist/user-agent.js +65 -0
  264. package/dist/word-wrapping.js +42 -0
  265. package/dist/workspace-manager.js +40 -0
  266. package/dist/workspace.js +120 -0
  267. package/help.md +97 -0
  268. package/package.json +138 -0
@@ -0,0 +1,42 @@
1
+ import { makeHarnessTab, distinctColor, uniqueLabel } from './tab/index.js';
2
+ import { parseSshCommand } from './ssh.js';
3
+ import { messageBus } from './bus.js';
4
+ // Owns the `ssh <destination> [options…]` command: launching a real `ssh` session as a
5
+ // full-tab PTY (reusing the harness-view tab shape, `HarnessView.destination` set). Mirrors
6
+ // `HarnessManager` — same tab creation, focus, and PTY-spawn flow — but with no `as`/`--workspace`
7
+ // clauses (both would collide with ssh's own argument grammar).
8
+ export class SshManager {
9
+ managers;
10
+ constructor(managers) {
11
+ this.managers = managers;
12
+ }
13
+ // Handle an `ssh <destination> [options…]` command. Returns an error message to surface in
14
+ // the creator's transcript, or undefined once the ssh tab has been opened.
15
+ run(input) {
16
+ const parsed = parseSshCommand(input);
17
+ if ('error' in parsed)
18
+ return parsed.error;
19
+ return this.open(parsed.command, parsed.destination, parsed.label);
20
+ }
21
+ // Open (and focus) an ssh tab labeled after the destination's host, running `command`
22
+ // (the verbatim `ssh …` invocation) in the creator's cwd.
23
+ open(command, destination, label_) {
24
+ const creator = this.managers.tab.cur();
25
+ const label = uniqueLabel(this.managers.tab.tabs, label_);
26
+ const cwd = this.managers.tab.cwdOf(creator.label) ?? process.cwd();
27
+ const dotColor = distinctColor(this.managers.tab.tabs.map((t) => t.dotColor));
28
+ const group = creator?.group ?? 1;
29
+ const groupColor = creator?.groupColor ?? dotColor;
30
+ const harness = { name: 'ssh', program: 'ssh', ptyId: '', status: 'running', destination };
31
+ const tab = makeHarnessTab(label, dotColor, this.managers.tab.tabs.length + 1, group, groupColor, harness);
32
+ this.managers.tab.insertTabInGroup(tab);
33
+ this.managers.tab.activeTab = this.managers.tab.findIndex(tab.label);
34
+ const id = this.managers.pty.spawn(label, 'ssh', command, cwd);
35
+ this.managers.harness.registerScreenReader(id);
36
+ const liveTab = this.managers.tab.tabs.find((t) => t.label === label);
37
+ if (liveTab?.harness)
38
+ liveTab.harness.ptyId = id;
39
+ messageBus.emit('state', { type: 'dirty' });
40
+ return undefined;
41
+ }
42
+ }
package/dist/ssh.js ADDED
@@ -0,0 +1,39 @@
1
+ // OpenSSH flags that take a following value, so the token after them is never mistaken for the
2
+ // destination (e.g. `ssh -p 2222 -i ~/.ssh/id admin@host` must still find `admin@host`).
3
+ const VALUE_FLAGS = new Set(['-B', '-b', '-c', '-D', '-E', '-e', '-F', '-I', '-i', '-J', '-L', '-l', '-m', '-O', '-o', '-p', '-Q', '-R', '-S', '-W', '-w']);
4
+ const SCHEME = /^ssh:\/\//i;
5
+ const USER_PREFIX = /^[^@]*@/;
6
+ const PORT_SUFFIX = /:\d+$/;
7
+ const USAGE = 'Usage: ssh <destination> [ssh options].';
8
+ // Find the first non-option token in an `ssh` command's arguments, skipping any flag and (for
9
+ // value-taking flags) the value that follows it.
10
+ function findDestination(tokens) {
11
+ let i = 0;
12
+ while (i < tokens.length) {
13
+ const token = tokens[i];
14
+ if (token.startsWith('-')) {
15
+ i += VALUE_FLAGS.has(token) ? 2 : 1;
16
+ continue;
17
+ }
18
+ return token;
19
+ }
20
+ return undefined;
21
+ }
22
+ /**
23
+ * Parse an `ssh <destination> [ssh options…]` command. `command` is the input verbatim
24
+ * (trimmed), spawned as-is — every flag, `user@host`, port, jump host, or trailing remote
25
+ * command works without modeling ssh's own CLI grammar. `destination` is the connection
26
+ * identity (scheme stripped, if any); `label` is its bare host, used as the tab label.
27
+ */
28
+ export function parseSshCommand(input) {
29
+ const trimmed = input.trim();
30
+ const rest = trimmed.replace(/^ssh\b\s*/i, '').trim();
31
+ if (!rest)
32
+ return { error: USAGE };
33
+ const destinationToken = findDestination(rest.split(/\s+/));
34
+ if (!destinationToken)
35
+ return { error: USAGE };
36
+ const destination = destinationToken.replace(SCHEME, '');
37
+ const label = destination.replace(USER_PREFIX, '').replace(PORT_SUFFIX, '');
38
+ return { command: trimmed, destination, label };
39
+ }
@@ -0,0 +1,21 @@
1
+ import { appVersion } from './cli-args.js';
2
+ export function explainStartupError(error, context = {}) {
3
+ const code = error?.code;
4
+ const port = context.port === undefined ? 'the requested port' : String(context.port);
5
+ if (code === 'EADDRINUSE') {
6
+ return `port ${port} is already in use.\n Another janus (or other app) is listening there. Pick another port with --port=<n>,\n or omit --port to choose a free port automatically.`;
7
+ }
8
+ if (code === 'EACCES') {
9
+ return `permission denied binding to port ${port}.\n Ports below 1024 need elevated privileges. Pick a port above 1024 with --port=<n>.`;
10
+ }
11
+ return null;
12
+ }
13
+ export function formatFatal(message) {
14
+ return `${appVersion()} — failed to start: ${message}`;
15
+ }
16
+ export function maybeStack(error) {
17
+ if (!process.env.JANUS_DEBUG)
18
+ return '';
19
+ const stack = error instanceof Error ? error.stack : undefined;
20
+ return stack ? `${stack}\n` : '';
21
+ }
@@ -0,0 +1,14 @@
1
+ import { getConfig } from './config.js';
2
+ import { globalCommands } from './global-history.js';
3
+ import { listTasks } from './tasks.js';
4
+ import { listProfiles } from './profiles.js';
5
+ // Full state snapshot sent on `init` and whenever anything changes — shared by index.ts's
6
+ // broadcast-driving emitState and message-handler.ts's `init` reply.
7
+ export function buildStateEvent(controller) {
8
+ return {
9
+ t: 'state', tabs: controller.view(), activeTab: controller.managers.tab.activeTab,
10
+ route: controller.routeView(), tabNameMaxLength: getConfig().tabNameMaxLength,
11
+ globalHistory: globalCommands(), syntaxTheme: getConfig().syntaxTheme, theme: getConfig().theme, tasks: listTasks(),
12
+ profiles: listProfiles(), projectDir: controller.rootDir,
13
+ };
14
+ }
@@ -0,0 +1,38 @@
1
+ // Format an agent's persisted state for the `state` command (ported from commands/state.ts).
2
+ function formatValue(v, maxLines = 10) {
3
+ if (v === undefined || v === null)
4
+ return '<empty>';
5
+ if (typeof v === 'string')
6
+ return v || '<empty>';
7
+ if (typeof v === 'boolean' || typeof v === 'number')
8
+ return String(v);
9
+ if (Array.isArray(v)) {
10
+ if (v.length === 0)
11
+ return '<empty>';
12
+ const lines = v.flatMap((item) => {
13
+ if (typeof item === 'object' && item !== null) {
14
+ const entry = item;
15
+ return [
16
+ `> ${entry.input ?? ''}`,
17
+ ...(typeof entry.output === 'string' && entry.output ? entry.output.split('\n').map((l) => ` ${l}`) : [' <empty>']),
18
+ ];
19
+ }
20
+ return [` - ${JSON.stringify(item)}`];
21
+ });
22
+ if (lines.length <= maxLines)
23
+ return lines.join('\n');
24
+ return `... (${lines.length - maxLines} lines omitted)\n${lines.slice(-maxLines).join('\n')}`;
25
+ }
26
+ if (typeof v === 'object') {
27
+ const lines = Object.entries(v).map(([k, value]) => ` ${k}: ${formatValue(value)}`);
28
+ if (lines.length <= maxLines)
29
+ return lines.join('\n');
30
+ return `... (${lines.length - maxLines} lines omitted)\n${lines.slice(-maxLines).join('\n')}`;
31
+ }
32
+ return String(v);
33
+ }
34
+ export function formatState(label, state) {
35
+ if (!state)
36
+ return `No state file found for "${label}".`;
37
+ return Object.entries(state).map(([k, v]) => `${k}:\n${formatValue(v)}`).join('\n\n');
38
+ }
@@ -0,0 +1,14 @@
1
+ // Names of the syntax-highlighting themes available for `syntax theme <name>`. Shared between the
2
+ // server (persisted config, command validation) and the web client (theme CSS lookup) via the
3
+ // `@shared` alias, so this file must stay free of Node imports.
4
+ export const SYNTAX_THEMES = [
5
+ 'github-dark',
6
+ 'github',
7
+ 'atom-one-dark',
8
+ 'atom-one-light',
9
+ 'monokai',
10
+ 'nord',
11
+ 'vs2015',
12
+ 'tokyo-night-dark',
13
+ ];
14
+ export const DEFAULT_SYNTAX_THEME = 'github-dark';
@@ -0,0 +1,19 @@
1
+ // Assembles the persisted-state snapshot for one tab — the shape saved to disk and restored by
2
+ // rehydrate() (see tab-rehydrate.ts) — from the tab itself plus the manager's per-label maps.
3
+ export function buildAgentStateFromTab(tab, busy, cwd, context, commandQueue, extra) {
4
+ return {
5
+ name: tab.label,
6
+ dotColor: tab.dotColor,
7
+ active: busy,
8
+ number: tab.number,
9
+ group: tab.group,
10
+ groupColor: tab.groupColor,
11
+ cmdHistory: tab.cmdHistory,
12
+ cwd,
13
+ context,
14
+ commandQueue,
15
+ title: tab.title,
16
+ offline: tab.offline,
17
+ ...extra,
18
+ };
19
+ }
@@ -0,0 +1,33 @@
1
+ import { messageBus } from '../bus.js';
2
+ export function closeTabResources(tab, managers, openFiles, context, queue, tabsLength) {
3
+ // Remove the workspace clone in the background: it is a recursive rmSync of a full git clone,
4
+ // slow enough to freeze the UI if run inline (the tab can't visibly close until it finishes).
5
+ // Deferring it lets the tab close and the state broadcast reach the client first. The clone stays
6
+ // tracked until `remove` runs, so a shutdown before this fires still cleans it up via removeAll().
7
+ if (tab.workspaceDir) {
8
+ const workspaceDir = tab.workspaceDir;
9
+ setTimeout(() => managers.workspace.remove(workspaceDir), 0);
10
+ }
11
+ managers.shell.close(tab.label);
12
+ managers.acp.close(tab.label);
13
+ managers.browser.closeTab(tab.label);
14
+ managers.pty.closeTab(tab.label);
15
+ managers.tab.deleteBusy(tab.label);
16
+ managers.fileTree.closeTab(tab.label);
17
+ managers.editorWatch.closeTab(tab.label);
18
+ managers.schedule.delete(tab.label);
19
+ managers.database.forgetTab(tab.label);
20
+ if (tabsLength <= 1)
21
+ managers.database.closeAll();
22
+ messageBus.emit('transcript', { type: 'tab:removed', tabLabel: tab.label });
23
+ if (tab.image) {
24
+ const id = tab.image.url.replace(/^\/open\//, '');
25
+ openFiles.delete(id);
26
+ }
27
+ if (tab.markdown) {
28
+ const id = tab.markdown.url.replace(/^\/open\//, '');
29
+ openFiles.delete(id);
30
+ }
31
+ context.delete(tab.label);
32
+ queue.delete(tab.label);
33
+ }
@@ -0,0 +1,43 @@
1
+ export const dotColors = [
2
+ '#5b9cff', '#ff6b6b', '#ffd93d', '#6bcb77', '#4d96ff',
3
+ '#ff8a5c', '#a66cff', '#ff6f91', '#00d2d3', '#f368e0',
4
+ '#ff9f43', '#54a0ff', '#5f27cd', '#01a3a4', '#ee5a24',
5
+ ];
6
+ const COLOR_MIN_DIST = 110;
7
+ function hexToRgb(hex) {
8
+ if (!hex)
9
+ return undefined;
10
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
11
+ if (!m)
12
+ return undefined;
13
+ const n = Number.parseInt(m[1], 16);
14
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
15
+ }
16
+ function colorDistance(a, b) {
17
+ const rmean = (a[0] + b[0]) / 2;
18
+ const dr = a[0] - b[0], dg = a[1] - b[1], db = a[2] - b[2];
19
+ return Math.sqrt((2 + rmean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rmean) / 256) * db * db);
20
+ }
21
+ function nearestUsedDistance(color, used) {
22
+ const rgb = hexToRgb(color);
23
+ if (!rgb)
24
+ return 0;
25
+ if (used.length === 0)
26
+ return Infinity;
27
+ return Math.min(...used.map((u) => colorDistance(rgb, u)));
28
+ }
29
+ export function distinctColor(used, preferred) {
30
+ const usedRgb = [...used].map((c) => hexToRgb(c)).filter((c) => c !== undefined);
31
+ if (preferred && nearestUsedDistance(preferred, usedRgb) >= COLOR_MIN_DIST)
32
+ return preferred;
33
+ let best = dotColors[0];
34
+ let bestDist = -1;
35
+ for (const c of dotColors) {
36
+ const d = nearestUsedDistance(c, usedRgb);
37
+ if (d > bestDist) {
38
+ bestDist = d;
39
+ best = c;
40
+ }
41
+ }
42
+ return best;
43
+ }
@@ -0,0 +1,95 @@
1
+ import { makeImageTab, makeMarkdownTab, makeEditorTab, makePageTab, makeFilesTab, makeNotificationsTab, distinctColor, insertTabInGroup, } from './index.js';
2
+ import { NOTIFICATIONS_LABEL } from '../notifications-tab.js';
3
+ import { getConfig } from '../config.js';
4
+ function uniqueLabel(used, prefix) {
5
+ if (!used.has(prefix))
6
+ return prefix;
7
+ let n = 2;
8
+ while (used.has(`${prefix}-${n}`))
9
+ n++;
10
+ return `${prefix}-${n}`;
11
+ }
12
+ export function uniqueImageLabel(tabs) {
13
+ return uniqueLabel(new Set(tabs.map((t) => t.label)), 'image');
14
+ }
15
+ export function uniqueMarkdownLabel(tabs) {
16
+ return uniqueLabel(new Set(tabs.map((t) => t.label)), 'markdown');
17
+ }
18
+ export function uniqueEditorLabel(tabs) {
19
+ return uniqueLabel(new Set(tabs.map((t) => t.label)), 'editor');
20
+ }
21
+ export function uniqueFilesLabel(tabs) {
22
+ return uniqueLabel(new Set(tabs.map((t) => t.label)), 'navigator');
23
+ }
24
+ export function uniquePageNumber(tabs) {
25
+ const used = new Set(tabs.filter((t) => t.page).map((t) => t.page.number));
26
+ let n = 1;
27
+ while (used.has(n))
28
+ n++;
29
+ return n;
30
+ }
31
+ export function addImageTab(tabs, activeTab, image) {
32
+ const creator = tabs[activeTab];
33
+ const label = uniqueImageLabel(tabs);
34
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
35
+ const group = creator?.group ?? 1;
36
+ const groupColor = creator?.groupColor ?? dotColor;
37
+ const tab = makeImageTab(label, dotColor, tabs.length + 1, group, groupColor, image);
38
+ tab.title = image.name.slice(0, getConfig().tabNameMaxLength);
39
+ const newTabs = insertTabInGroup(tabs, tab);
40
+ return { tabs: newTabs, activeTab: newTabs.findIndex((t) => t.label === label) };
41
+ }
42
+ function finalizeTab(tabs, tab, label, title) {
43
+ tab.title = title.slice(0, getConfig().tabNameMaxLength);
44
+ const newTabs = insertTabInGroup(tabs, tab);
45
+ return { tabs: newTabs, activeTab: newTabs.findIndex((t) => t.label === label) };
46
+ }
47
+ export function addMarkdownTab(tabs, activeTab, view) {
48
+ const creator = tabs[activeTab];
49
+ const label = uniqueMarkdownLabel(tabs);
50
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
51
+ const group = creator?.group ?? 1;
52
+ const groupColor = creator?.groupColor ?? dotColor;
53
+ const tab = makeMarkdownTab(label, dotColor, tabs.length + 1, group, groupColor, view);
54
+ return finalizeTab(tabs, tab, label, view.name);
55
+ }
56
+ export function addEditorTab(tabs, activeTab, view) {
57
+ const creator = tabs[activeTab];
58
+ const label = uniqueEditorLabel(tabs);
59
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
60
+ const group = creator?.group ?? 1;
61
+ const groupColor = creator?.groupColor ?? dotColor;
62
+ const tab = makeEditorTab(label, dotColor, tabs.length + 1, group, groupColor, view);
63
+ return finalizeTab(tabs, tab, label, view.name);
64
+ }
65
+ export function addFilesTab(tabs, activeTab, view) {
66
+ const creator = tabs[activeTab];
67
+ const label = uniqueFilesLabel(tabs);
68
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
69
+ const group = creator?.group ?? 1;
70
+ const groupColor = creator?.groupColor ?? dotColor;
71
+ const tab = makeFilesTab(label, dotColor, tabs.length + 1, group, groupColor, view);
72
+ const newTabs = insertTabInGroup(tabs, tab, 'start');
73
+ return { tabs: newTabs, activeTab: newTabs.findIndex((t) => t.label === label) };
74
+ }
75
+ export function addNotificationsTab(tabs, activeTab) {
76
+ const creator = tabs[activeTab];
77
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
78
+ const group = creator?.group ?? 1;
79
+ const groupColor = creator?.groupColor ?? dotColor;
80
+ const tab = makeNotificationsTab(NOTIFICATIONS_LABEL, dotColor, tabs.length + 1, group, groupColor);
81
+ const newTabs = insertTabInGroup(tabs, tab, 'start');
82
+ return { tabs: newTabs, activeTab: newTabs.findIndex((t) => t.label === NOTIFICATIONS_LABEL) };
83
+ }
84
+ export function addPageTab(tabs, activeTab, url, domain) {
85
+ const creator = tabs[activeTab];
86
+ const number = uniquePageNumber(tabs);
87
+ const label = `page-${number}`;
88
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
89
+ const group = creator?.group ?? 1;
90
+ const groupColor = creator?.groupColor ?? dotColor;
91
+ const page = { url, domain, number };
92
+ const tab = makePageTab(label, dotColor, tabs.length + 1, group, groupColor, page);
93
+ const newTabs = insertTabInGroup(tabs, tab);
94
+ return { tabs: newTabs, activeTab: newTabs.findIndex((t) => t.label === label) };
95
+ }
@@ -0,0 +1,35 @@
1
+ // Resolves docking a tab into a sidebar (`'left'` | `'right'`), or undocking it back to the
2
+ // center strip (`null`, which also makes it the active tab). Docking into a side that already
3
+ // holds a tab of the *same view kind* displaces that occupant back to center (non-destructive —
4
+ // nothing closes); a different-kind occupant (the file navigator and notifications tab share a
5
+ // sidebar via the client's own tab-switcher) is left docked. Mutates `tab.dock`/`tab.hasUnread`
6
+ // in place (matching the rest of TabManager's per-tab field mutation) and returns the active tab
7
+ // index the caller should adopt. `recordLeavingActiveTab` is invoked exactly where the caller's
8
+ // focus-history bookkeeping expects it — right before the active tab actually changes.
9
+ export function applyDock(tabs, activeTab, index, dock, recordLeavingActiveTab) {
10
+ const tab = tabs[index];
11
+ if (!tab)
12
+ return activeTab;
13
+ if (dock === null) {
14
+ tab.dock = undefined;
15
+ recordLeavingActiveTab(index);
16
+ tab.hasUnread = false;
17
+ return index;
18
+ }
19
+ const occupant = tabs.find((t, i) => i !== index && t.dock === dock && t.view === tab.view);
20
+ if (occupant)
21
+ occupant.dock = undefined;
22
+ tab.dock = dock;
23
+ return activeTab === index ? nearestNonDocked(tabs, activeTab, recordLeavingActiveTab) : activeTab;
24
+ }
25
+ function nearestNonDocked(tabs, activeTab, recordLeavingActiveTab) {
26
+ const total = tabs.length;
27
+ for (let step = 0; step < total; step++) {
28
+ const index = (activeTab + step) % total;
29
+ if (!tabs[index]?.dock) {
30
+ recordLeavingActiveTab(index);
31
+ return index;
32
+ }
33
+ }
34
+ return activeTab;
35
+ }
@@ -0,0 +1,41 @@
1
+ // Tracks which tab was active before the current one, so closing or undocking a tab can restore
2
+ // focus to its predecessor rather than just clamping to the nearest surviving index. Pure
3
+ // operations on the `tabs`/`focusHistory` state; TabManager owns the state itself.
4
+ // Records that `activeTab` is about to stop being active in favor of `newIndex`, so closing
5
+ // `newIndex` later can restore focus to it. No-ops (returns `focusHistory` unchanged) if
6
+ // `newIndex` is already active.
7
+ export function recordLeavingActiveTab(tabs, activeTab, focusHistory, newIndex) {
8
+ if (newIndex === activeTab)
9
+ return focusHistory;
10
+ const leaving = tabs[activeTab]?.label;
11
+ if (!leaving)
12
+ return focusHistory;
13
+ return [...focusHistory.filter((l) => l !== leaving), leaving];
14
+ }
15
+ // Pops the most recent still-valid (existing, non-docked) label off the focus-history stack,
16
+ // discarding any stale entries (closed or since-docked tabs) along the way. Returns the resolved
17
+ // tab index (or `undefined` if none remain) and the history with all consumed entries removed.
18
+ export function popFocusHistory(tabs, focusHistory) {
19
+ const history = [...focusHistory];
20
+ while (history.length > 0) {
21
+ const label = history.pop();
22
+ const index = tabs.findIndex((t) => t.label === label);
23
+ if (index !== -1 && !tabs[index].dock)
24
+ return { index, history };
25
+ }
26
+ return { index: undefined, history };
27
+ }
28
+ // The label of the file-tree tab to retarget when the metadata-row 📁 button is clicked: the
29
+ // most-recently-left still-open `view === 'files'` tab. Scans `focusHistory` from most-recent to
30
+ // least-recent without mutating it and — unlike `popFocusHistory` — includes docked tabs, since a
31
+ // docked file navigator is a valid retarget target. Falls back to the first `view === 'files'`
32
+ // tab in `tabs` order (e.g. a tree that never lost focus since opening); returns `undefined` when
33
+ // no file-tree tab exists.
34
+ export function mostRecentFileTreeLabel(tabs, focusHistory) {
35
+ for (let i = focusHistory.length - 1; i >= 0; i--) {
36
+ const tab = tabs.find((t) => t.label === focusHistory[i]);
37
+ if (tab?.view === 'files')
38
+ return tab.label;
39
+ }
40
+ return tabs.find((t) => t.view === 'files')?.label;
41
+ }
@@ -0,0 +1,77 @@
1
+ import { formatMessageContent, tryCollapseToolSteps } from '../buffer.js';
2
+ export function expandTabs(text, tabWidth = 8) {
3
+ if (!text.includes('\t'))
4
+ return text;
5
+ let col = 0;
6
+ let out = '';
7
+ for (const ch of text) {
8
+ if (ch === '\t') {
9
+ const spaces = tabWidth - (col % tabWidth);
10
+ out += ' '.repeat(spaces);
11
+ col += spaces;
12
+ }
13
+ else {
14
+ out += ch;
15
+ col += ch === '\n' ? 0 : 1;
16
+ }
17
+ }
18
+ return out;
19
+ }
20
+ export function handleCollapsedToolSteps(log, index, lines, shouldCollapseToolSteps) {
21
+ if (!shouldCollapseToolSteps)
22
+ return { handled: false, newIndex: index };
23
+ const collapse = tryCollapseToolSteps(log, index);
24
+ if (!collapse)
25
+ return { handled: false, newIndex: index };
26
+ if (lines.length > 0)
27
+ lines.push({ type: 'spacer', text: '' });
28
+ lines.push({
29
+ type: 'collapsed',
30
+ text: `${collapse.count} tool step${collapse.count === 1 ? '' : 's'}`,
31
+ acp: true,
32
+ });
33
+ return { handled: true, newIndex: collapse.newIndex };
34
+ }
35
+ export function handleTerminalEntry(entry, lines) {
36
+ if (!entry.terminal)
37
+ return false;
38
+ if (lines.length > 0)
39
+ lines.push({ type: 'spacer', text: '' });
40
+ lines.push({ type: 'terminal', text: '', terminal: entry.terminal });
41
+ return true;
42
+ }
43
+ export function handleMessageEntry(entry, lines) {
44
+ if (!entry.from)
45
+ return false;
46
+ if (lines.length > 0)
47
+ lines.push({ type: 'spacer', text: '' });
48
+ const parts = entry.output.split('\n');
49
+ lines.push(...formatMessageContent(entry, parts));
50
+ return true;
51
+ }
52
+ export function handleInputOutput(entry, lines) {
53
+ if (!entry.input && !entry.output)
54
+ return false;
55
+ if (lines.length > 0)
56
+ lines.push({ type: 'spacer', text: '' });
57
+ if (entry.input) {
58
+ lines.push({
59
+ type: 'prompt',
60
+ text: expandTabs(entry.input),
61
+ cwd: entry.cwd,
62
+ acp: entry.acp,
63
+ running: entry.running,
64
+ });
65
+ }
66
+ if (entry.output) {
67
+ if (entry.markdown) {
68
+ lines.push({ type: 'markdown', text: entry.output });
69
+ }
70
+ else {
71
+ for (const outLine of entry.output.split('\n')) {
72
+ lines.push({ type: 'output', text: expandTabs(outLine), acp: entry.acp });
73
+ }
74
+ }
75
+ }
76
+ return true;
77
+ }
@@ -0,0 +1,21 @@
1
+ import { handleCollapsedToolSteps, handleTerminalEntry, handleMessageEntry, handleInputOutput } from './formatting-handlers.js';
2
+ export { expandTabs } from './formatting-handlers.js';
3
+ export { wordWrap } from '../word-wrapping.js';
4
+ export function flattenBuffer(log, shouldCollapseToolSteps = false) {
5
+ const lines = [];
6
+ for (let index = 0; index < log.length; index++) {
7
+ const entry = log[index];
8
+ const collapsed = handleCollapsedToolSteps(log, index, lines, shouldCollapseToolSteps);
9
+ if (collapsed.handled) {
10
+ index = collapsed.newIndex;
11
+ continue;
12
+ }
13
+ if (handleTerminalEntry(entry, lines))
14
+ continue;
15
+ if (handleMessageEntry(entry, lines))
16
+ continue;
17
+ if (handleInputOutput(entry, lines))
18
+ continue;
19
+ }
20
+ return lines;
21
+ }
@@ -0,0 +1,14 @@
1
+ import { stripComments } from './index.js';
2
+ // Appends `text` (after stripping shell comments) to `tab`'s command history, deduping an
3
+ // immediate repeat and capping at 100 entries, and resets the history-scroll cursor. Returns the
4
+ // stripped text for the caller to actually run.
5
+ export function recordHistory(tab, text) {
6
+ const trimmed = stripComments(text);
7
+ if (tab) {
8
+ if (trimmed && tab.cmdHistory.at(-1) !== trimmed) {
9
+ tab.cmdHistory = [...tab.cmdHistory, trimmed].slice(-100);
10
+ }
11
+ tab.cmdHistoryIdx = -1;
12
+ }
13
+ return trimmed;
14
+ }
@@ -0,0 +1,65 @@
1
+ export { expandTabs, wordWrap, flattenBuffer } from './formatting.js';
2
+ export { distinctColor, dotColors } from './colors.js';
3
+ export { stripComments, renumberTabs, canMoveTab, swapTabsLeft, swapTabsRight, insertTabInGroup, uniqueLabel } from './utils.js';
4
+ export const makeTab = (label, dotColor, number = 1, commandHistory = [], log = [], workspaceDirectory, group = 1, groupColor = dotColor) => ({
5
+ label,
6
+ dotColor,
7
+ number,
8
+ group,
9
+ groupColor,
10
+ log,
11
+ cmdHistory: commandHistory,
12
+ cmdHistoryIdx: -1,
13
+ scrollOffset: 0,
14
+ workspaceDir: workspaceDirectory,
15
+ });
16
+ // An image view tab (opened via `open <image>`). It carries no transcript/history/shell — just the
17
+ // image payload — and is always titled `image` while keeping a unique `label` so several can coexist.
18
+ export const makeImageTab = (label, dotColor, number, group, groupColor, image) => ({
19
+ ...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
20
+ view: 'image',
21
+ title: 'image',
22
+ image,
23
+ });
24
+ // A page view tab (opened via `open https://…` or `open page …`). Renders an iframe; carries no
25
+ // transcript/history/shell. The title shows the domain (e.g. "slashdot.org").
26
+ export const makePageTab = (label, dotColor, number, group, groupColor, page) => ({
27
+ ...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
28
+ view: 'page',
29
+ title: page.domain,
30
+ page,
31
+ });
32
+ // A markdown view tab (opened via `open <file>.md`). Renders the file as formatted Markdown.
33
+ export const makeMarkdownTab = (label, dotColor, number, group, groupColor, markdown) => ({
34
+ ...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
35
+ view: 'markdown',
36
+ title: 'markdown',
37
+ markdown,
38
+ });
39
+ // An editor view tab (opened via `open <text file>` or `edit <file>`). Hosts the plain-text editor.
40
+ export const makeEditorTab = (label, dotColor, number, group, groupColor, editor) => ({
41
+ ...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
42
+ view: 'editor',
43
+ title: editor.name,
44
+ editor,
45
+ });
46
+ // A harness view tab (opened via `harness <name>`). The entire tab body is a live PTY terminal.
47
+ export const makeHarnessTab = (label, dotColor, number, group, groupColor, harness, workspaceDirectory) => ({
48
+ ...makeTab(label, dotColor, number, [], [], workspaceDirectory, group, groupColor),
49
+ view: 'harness', title: label, harness,
50
+ });
51
+ // A file tree view tab (opened via `files [path]`). Shows a directory tree rooted at `files.root`.
52
+ export const makeFilesTab = (label, dotColor, number, group, groupColor, files) => ({
53
+ ...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
54
+ view: 'files',
55
+ title: 'navigator',
56
+ files,
57
+ });
58
+ // A notifications view tab (opened via `notifications`). A singleton, view-only feed whose body is
59
+ // the standard transcript fed by its own `log`; it takes no typed input. Live and in-memory like
60
+ // the file tree tab — never persisted, never restored on `--relaunch`.
61
+ export const makeNotificationsTab = (label, dotColor, number, group, groupColor) => ({
62
+ ...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
63
+ view: 'notifications',
64
+ title: 'notifications',
65
+ });