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,85 @@
1
+ import { makeImageTab, makeMarkdownTab, makeEditorTab, makePageTab, makeFilesTab, distinctColor, insertTabInGroup, } from './tab.js';
2
+ import { getConfig } from './config.js';
3
+ function uniqueLabel(used, prefix) {
4
+ if (!used.has(prefix))
5
+ return prefix;
6
+ let n = 2;
7
+ while (used.has(`${prefix}-${n}`))
8
+ n++;
9
+ return `${prefix}-${n}`;
10
+ }
11
+ export function uniqueImageLabel(tabs) {
12
+ return uniqueLabel(new Set(tabs.map((t) => t.label)), 'image');
13
+ }
14
+ export function uniqueMarkdownLabel(tabs) {
15
+ return uniqueLabel(new Set(tabs.map((t) => t.label)), 'markdown');
16
+ }
17
+ export function uniqueEditorLabel(tabs) {
18
+ return uniqueLabel(new Set(tabs.map((t) => t.label)), 'editor');
19
+ }
20
+ export function uniqueFilesLabel(tabs) {
21
+ return uniqueLabel(new Set(tabs.map((t) => t.label)), 'navigator');
22
+ }
23
+ export function uniquePageNumber(tabs) {
24
+ const used = new Set(tabs.filter((t) => t.page).map((t) => t.page.number));
25
+ let n = 1;
26
+ while (used.has(n))
27
+ n++;
28
+ return n;
29
+ }
30
+ export function addImageTab(tabs, activeTab, image) {
31
+ const creator = tabs[activeTab];
32
+ const label = uniqueImageLabel(tabs);
33
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
34
+ const group = creator?.group ?? 1;
35
+ const groupColor = creator?.groupColor ?? dotColor;
36
+ const tab = makeImageTab(label, dotColor, tabs.length + 1, group, groupColor, image);
37
+ tab.title = image.name.slice(0, getConfig().tabNameMaxLength);
38
+ const newTabs = insertTabInGroup(tabs, tab);
39
+ return { tabs: newTabs, activeTab: newTabs.findIndex((t) => t.label === label) };
40
+ }
41
+ function finalizeTab(tabs, tab, label, title) {
42
+ tab.title = title.slice(0, getConfig().tabNameMaxLength);
43
+ const newTabs = insertTabInGroup(tabs, tab);
44
+ return { tabs: newTabs, activeTab: newTabs.findIndex((t) => t.label === label) };
45
+ }
46
+ export function addMarkdownTab(tabs, activeTab, view) {
47
+ const creator = tabs[activeTab];
48
+ const label = uniqueMarkdownLabel(tabs);
49
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
50
+ const group = creator?.group ?? 1;
51
+ const groupColor = creator?.groupColor ?? dotColor;
52
+ const tab = makeMarkdownTab(label, dotColor, tabs.length + 1, group, groupColor, view);
53
+ return finalizeTab(tabs, tab, label, view.name);
54
+ }
55
+ export function addEditorTab(tabs, activeTab, view) {
56
+ const creator = tabs[activeTab];
57
+ const label = uniqueEditorLabel(tabs);
58
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
59
+ const group = creator?.group ?? 1;
60
+ const groupColor = creator?.groupColor ?? dotColor;
61
+ const tab = makeEditorTab(label, dotColor, tabs.length + 1, group, groupColor, view);
62
+ return finalizeTab(tabs, tab, label, view.name);
63
+ }
64
+ export function addFilesTab(tabs, activeTab, view) {
65
+ const creator = tabs[activeTab];
66
+ const label = uniqueFilesLabel(tabs);
67
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
68
+ const group = creator?.group ?? 1;
69
+ const groupColor = creator?.groupColor ?? dotColor;
70
+ const tab = makeFilesTab(label, dotColor, tabs.length + 1, group, groupColor, view);
71
+ const newTabs = insertTabInGroup(tabs, tab, 'start');
72
+ return { tabs: newTabs, activeTab: newTabs.findIndex((t) => t.label === label) };
73
+ }
74
+ export function addPageTab(tabs, activeTab, url, domain) {
75
+ const creator = tabs[activeTab];
76
+ const number = uniquePageNumber(tabs);
77
+ const label = `page-${number}`;
78
+ const dotColor = distinctColor(tabs.map((t) => t.dotColor));
79
+ const group = creator?.group ?? 1;
80
+ const groupColor = creator?.groupColor ?? dotColor;
81
+ const page = { url, domain, number };
82
+ const tab = makePageTab(label, dotColor, tabs.length + 1, group, groupColor, page);
83
+ const newTabs = insertTabInGroup(tabs, tab);
84
+ return { tabs: newTabs, activeTab: newTabs.findIndex((t) => t.label === label) };
85
+ }
@@ -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 './tab-formatting-handlers.js';
2
+ export { expandTabs } from './tab-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,418 @@
1
+ import { makeTab, distinctColor, insertTabInGroup, flattenBuffer, swapTabsLeft, swapTabsRight, stripComments, } from './tab.js';
2
+ import { saveAgentState, listAgentStates } from './agent-state.js';
3
+ import { abbreviatePath } from './paths.js';
4
+ import { getConfig } from './config.js';
5
+ import { messageBus } from './bus.js';
6
+ import { closeTabResources } from './tab-cleanup.js';
7
+ import { addImageTab, addMarkdownTab, addEditorTab, addPageTab, addFilesTab, } from './tab-creators.js';
8
+ export class TabManager {
9
+ managers;
10
+ tabs = [];
11
+ activeTab = 0;
12
+ cwd = new Map();
13
+ busy = new Set();
14
+ context = new Map();
15
+ queue = new Map();
16
+ onIdle = null;
17
+ openFiles = new Map();
18
+ openFileCounter = 0;
19
+ rootDir;
20
+ get launchDir() { return this.rootDir; }
21
+ static OPEN_MAX_FILES = 10;
22
+ constructor(managers, projectDir) {
23
+ this.managers = managers;
24
+ this.rootDir = projectDir ?? process.cwd();
25
+ this.tabs = [this.makeRootTab()];
26
+ this.cwd.set('janus', this.rootDir);
27
+ }
28
+ cur() {
29
+ return this.tabs[this.activeTab] ?? this.tabs[0];
30
+ }
31
+ allLabels() {
32
+ return this.tabs.map((t) => t.label);
33
+ }
34
+ isBusy(label) {
35
+ return this.busy.has(label);
36
+ }
37
+ cwdOf(label) {
38
+ return this.cwd.get(label);
39
+ }
40
+ setCwd(label, dir) {
41
+ this.cwd.set(label, dir);
42
+ }
43
+ addBusy(label) {
44
+ this.busy.add(label);
45
+ }
46
+ deleteBusy(label) {
47
+ this.busy.delete(label);
48
+ if (this.queueFor(label).length > 0) {
49
+ queueMicrotask(() => this.onIdle?.(label));
50
+ }
51
+ }
52
+ setOnIdle(hook) {
53
+ this.onIdle = hook;
54
+ }
55
+ queueFor(label) {
56
+ return this.queue.get(label) ?? [];
57
+ }
58
+ enqueue(label, text) {
59
+ this.queue.set(label, [...this.queueFor(label), text]);
60
+ this.persistQueue(label);
61
+ }
62
+ dequeue(label) {
63
+ const q = this.queueFor(label);
64
+ if (q.length === 0)
65
+ return undefined;
66
+ const [front, ...rest] = q;
67
+ this.queue.set(label, rest);
68
+ this.persistQueue(label);
69
+ return front;
70
+ }
71
+ editQueued(label, index, text) {
72
+ const q = this.queueFor(label);
73
+ if (index < 0 || index >= q.length)
74
+ return;
75
+ const next = [...q];
76
+ next[index] = text;
77
+ this.queue.set(label, next);
78
+ this.persistQueue(label);
79
+ }
80
+ deleteQueued(label, index) {
81
+ const q = this.queueFor(label);
82
+ if (index < 0 || index >= q.length)
83
+ return;
84
+ this.queue.set(label, q.filter((_, i) => i !== index));
85
+ this.persistQueue(label);
86
+ }
87
+ persistQueue(label) {
88
+ const tab = this.tabs.find((t) => t.label === label);
89
+ if (tab)
90
+ this.persist(this.buildAgentState(tab));
91
+ messageBus.emit('state', { type: 'dirty' });
92
+ }
93
+ contextFor(label) {
94
+ return this.context.get(label) ?? [];
95
+ }
96
+ setContext(label, ctx) {
97
+ this.context.set(label, ctx);
98
+ }
99
+ appendContext(label, text) {
100
+ this.context.set(label, [...(this.context.get(label) ?? []), text]);
101
+ }
102
+ findIndex(label) {
103
+ return this.tabs.findIndex((t) => t.label === label);
104
+ }
105
+ persist(state) {
106
+ try {
107
+ saveAgentState(state);
108
+ }
109
+ catch { /* ignore */ }
110
+ }
111
+ buildAgentState(tab, extra) {
112
+ return {
113
+ name: tab.label,
114
+ dotColor: tab.dotColor,
115
+ active: this.busy.has(tab.label),
116
+ number: tab.number,
117
+ group: tab.group,
118
+ groupColor: tab.groupColor,
119
+ cmdHistory: tab.cmdHistory,
120
+ cwd: this.cwd.get(tab.label),
121
+ context: this.context.get(tab.label),
122
+ commandQueue: this.queue.get(tab.label),
123
+ title: tab.title,
124
+ offline: tab.offline,
125
+ ...extra,
126
+ };
127
+ }
128
+ activeLabel() {
129
+ return this.tabs[this.activeTab]?.label;
130
+ }
131
+ markUnread(label) {
132
+ if (label === this.activeLabel())
133
+ return;
134
+ const tab = this.tabs.find((t) => t.label === label);
135
+ if (tab)
136
+ tab.hasUnread = true;
137
+ }
138
+ setActiveTab(index) {
139
+ if (index < 0 || index >= this.tabs.length)
140
+ return;
141
+ if (this.tabs[index]?.dock)
142
+ return; // a docked tab is never the active tab
143
+ this.activeTab = index;
144
+ const tab = this.tabs[index];
145
+ if (tab)
146
+ tab.hasUnread = false;
147
+ messageBus.emit('state', { type: 'dirty' });
148
+ }
149
+ moveTab(dir) {
150
+ const total = this.tabs.length;
151
+ for (let step = 1; step <= total; step++) {
152
+ const index = (this.activeTab + dir * step + total) % total;
153
+ if (!this.tabs[index]?.dock) {
154
+ this.setActiveTab(index);
155
+ return;
156
+ }
157
+ }
158
+ }
159
+ // Dock a tab into a sidebar (`'left'` | `'right'`), or undock it back to the center strip
160
+ // (`null`, which also makes it the active tab). Docking into an occupied side displaces the
161
+ // previous occupant back to center (non-destructive — nothing closes). Docking the active tab
162
+ // first moves `activeTab` to the nearest non-docked tab, preserving the invariant that a
163
+ // docked tab is never active.
164
+ setDock(index, dock) {
165
+ const tab = this.tabs[index];
166
+ if (!tab)
167
+ return;
168
+ if (dock === null) {
169
+ tab.dock = undefined;
170
+ this.activeTab = index;
171
+ tab.hasUnread = false;
172
+ messageBus.emit('state', { type: 'dirty' });
173
+ return;
174
+ }
175
+ const occupant = this.tabs.find((t, i) => i !== index && t.dock === dock);
176
+ if (occupant)
177
+ occupant.dock = undefined;
178
+ tab.dock = dock;
179
+ if (this.activeTab === index)
180
+ this.activateNearestNonDocked();
181
+ messageBus.emit('state', { type: 'dirty' });
182
+ }
183
+ activateNearestNonDocked() {
184
+ const total = this.tabs.length;
185
+ for (let step = 0; step < total; step++) {
186
+ const index = (this.activeTab + step) % total;
187
+ if (!this.tabs[index]?.dock) {
188
+ this.activeTab = index;
189
+ return;
190
+ }
191
+ }
192
+ }
193
+ reorderTab(dir) {
194
+ const from = this.activeTab;
195
+ const next = dir < 0 ? swapTabsLeft(this.tabs, from) : swapTabsRight(this.tabs, from);
196
+ if (next === this.tabs)
197
+ return;
198
+ this.tabs = next;
199
+ const to = dir < 0 ? Math.max(0, from - 1) : Math.min(from + 1, this.tabs.length - 1);
200
+ this.activeTab = to;
201
+ const active = this.tabs[to];
202
+ if (active)
203
+ active.hasUnread = false;
204
+ this.persist(this.buildAgentState(this.tabs[from]));
205
+ this.persist(this.buildAgentState(this.tabs[to]));
206
+ messageBus.emit('state', { type: 'dirty' });
207
+ }
208
+ closeTab(index) {
209
+ const tab = this.tabs[index];
210
+ if (!tab)
211
+ return;
212
+ const nonDockedCount = this.tabs.filter((t) => !t.dock).length;
213
+ closeTabResources(tab, this.managers, this.openFiles, this.context, this.queue, nonDockedCount);
214
+ // Closing the last remaining non-docked tab quits the app (same as the `quit` command).
215
+ if (!tab.dock && nonDockedCount <= 1) {
216
+ messageBus.emit('app', { type: 'exit' });
217
+ return;
218
+ }
219
+ this.tabs = this.tabs.filter((_, index_) => index_ !== index).map((t, index_) => ({ ...t, number: index_ + 1 }));
220
+ this.activeTab = Math.min(this.activeTab, this.tabs.length - 1);
221
+ const active = this.tabs[this.activeTab];
222
+ if (active)
223
+ active.hasUnread = false;
224
+ messageBus.emit('state', { type: 'dirty' });
225
+ }
226
+ renameTab(index, title) {
227
+ const tab = this.tabs[index];
228
+ if (!tab)
229
+ return;
230
+ const trimmed = title.trim().slice(0, getConfig().tabNameMaxLength);
231
+ if (trimmed && trimmed !== tab.label)
232
+ tab.title = trimmed;
233
+ else
234
+ delete tab.title;
235
+ this.persist(this.buildAgentState(tab));
236
+ messageBus.emit('state', { type: 'dirty' });
237
+ }
238
+ toggleCollapse() {
239
+ const tab = this.cur();
240
+ tab.toolStepsExpanded = !tab.toolStepsExpanded;
241
+ messageBus.emit('state', { type: 'dirty' });
242
+ }
243
+ insertTabInGroup(tab) {
244
+ this.tabs = insertTabInGroup(this.tabs, tab);
245
+ }
246
+ makeRootTab() {
247
+ const tab = makeTab('janus', distinctColor([]));
248
+ tab.toolStepsExpanded = false;
249
+ return tab;
250
+ }
251
+ startRunning(label, input) {
252
+ this.busy.add(label);
253
+ this.append(label, { input, output: '', running: true });
254
+ }
255
+ finishRunning(label, output) {
256
+ const t = this.tabs.find((x) => x.label === label);
257
+ if (t) {
258
+ const log = [...t.log];
259
+ const index = log.findLastIndex((e) => e.running);
260
+ if (index !== -1)
261
+ log[index] = { ...log[index], output, running: false };
262
+ t.log = log;
263
+ this.deleteBusy(label);
264
+ this.persist(this.buildAgentState(t));
265
+ }
266
+ if (output && t) {
267
+ messageBus.emit('transcript', {
268
+ type: 'entry:appended', tabLabel: label, entry: { input: '', output }, tab: t,
269
+ });
270
+ }
271
+ this.markUnread(label);
272
+ messageBus.emit('state', { type: 'dirty' });
273
+ }
274
+ capLog(log) {
275
+ const max = getConfig().transcriptMaxLines;
276
+ return log.length > max ? log.slice(log.length - max) : log;
277
+ }
278
+ append(label, entry) {
279
+ const tab = this.tabs.find((t) => t.label === label);
280
+ if (!tab)
281
+ return;
282
+ const before = tab.log.length;
283
+ tab.log = this.capLog([...tab.log, entry]);
284
+ tab.scrollOffset = 0;
285
+ const trimmed = before + 1 - tab.log.length;
286
+ if (trimmed > 0)
287
+ messageBus.emit('transcript', { type: 'entries:trimmed', tabLabel: label, count: trimmed });
288
+ messageBus.emit('transcript', { type: 'entry:appended', tabLabel: label, entry, tab });
289
+ this.markUnread(label);
290
+ messageBus.emit('state', { type: 'dirty' });
291
+ }
292
+ clearTranscript(label) {
293
+ const tab = this.tabs.find((t) => t.label === label);
294
+ if (!tab)
295
+ return;
296
+ tab.log = [];
297
+ this.persist(this.buildAgentState(tab));
298
+ messageBus.emit('transcript', { type: 'tab:cleared', tabLabel: label });
299
+ messageBus.emit('state', { type: 'dirty' });
300
+ }
301
+ recordHistory(index, text) {
302
+ const trimmed = stripComments(text);
303
+ const tab = this.tabs[index];
304
+ if (tab) {
305
+ if (trimmed && tab.cmdHistory.at(-1) !== trimmed) {
306
+ tab.cmdHistory = [...tab.cmdHistory, trimmed].slice(-100);
307
+ }
308
+ tab.cmdHistoryIdx = -1;
309
+ }
310
+ return trimmed;
311
+ }
312
+ shorten(p) {
313
+ return abbreviatePath(p, { root: this.rootDir });
314
+ }
315
+ registerFile(absPath) {
316
+ const id = String(++this.openFileCounter);
317
+ this.openFiles.set(id, absPath);
318
+ return `/open/${id}`;
319
+ }
320
+ openFilePath(id) {
321
+ return this.openFiles.get(id);
322
+ }
323
+ view(connectionsFor, acpLabel, scheduleView) {
324
+ return this.tabs.map((t) => ({
325
+ label: t.label,
326
+ number: t.number,
327
+ dotColor: t.dotColor,
328
+ group: t.group,
329
+ groupColor: t.groupColor,
330
+ busy: this.busy.has(t.label),
331
+ hasUnread: !!t.hasUnread,
332
+ cwd: this.cwd.get(t.label) ?? process.cwd(),
333
+ acp: acpLabel(t.label),
334
+ connections: connectionsFor(t.label),
335
+ schedule: scheduleView(t.label),
336
+ bufferLines: flattenBuffer(t.log, !t.toolStepsExpanded)
337
+ .map((l) => (l.cwd ? { ...l, cwd: this.shorten(l.cwd) } : l)),
338
+ cmdHistory: t.cmdHistory,
339
+ commandQueue: this.queue.get(t.label) ?? [],
340
+ toolStepsExpanded: !!t.toolStepsExpanded,
341
+ view: t.view,
342
+ title: t.title,
343
+ image: t.image,
344
+ page: t.page,
345
+ harness: t.harness,
346
+ markdown: t.markdown,
347
+ editor: t.editor,
348
+ monitor: t.monitor,
349
+ files: t.files ? { ...t.files, root: this.shorten(t.files.root) } : undefined,
350
+ activePty: t.activePty,
351
+ dock: t.dock,
352
+ }));
353
+ }
354
+ openImageTab(image) {
355
+ const { tabs, activeTab } = addImageTab(this.tabs, this.activeTab, image);
356
+ this.tabs = tabs;
357
+ this.activeTab = activeTab;
358
+ messageBus.emit('state', { type: 'dirty' });
359
+ }
360
+ openMarkdownTab(view) {
361
+ const { tabs, activeTab } = addMarkdownTab(this.tabs, this.activeTab, view);
362
+ this.tabs = tabs;
363
+ this.activeTab = activeTab;
364
+ messageBus.emit('state', { type: 'dirty' });
365
+ }
366
+ openEditorTab(view) {
367
+ const existing = this.tabs.find((t) => t.editor?.path === view.path);
368
+ if (existing) {
369
+ if (view.line !== undefined)
370
+ existing.editor.line = view.line;
371
+ this.setActiveTab(this.tabs.indexOf(existing));
372
+ messageBus.emit('state', { type: 'dirty' });
373
+ return;
374
+ }
375
+ const { tabs, activeTab } = addEditorTab(this.tabs, this.activeTab, view);
376
+ this.tabs = tabs;
377
+ this.activeTab = activeTab;
378
+ this.managers.editorWatch.watch(tabs[activeTab].label, view.path);
379
+ messageBus.emit('state', { type: 'dirty' });
380
+ }
381
+ openPageTab({ url, domain }) {
382
+ const { tabs, activeTab } = addPageTab(this.tabs, this.activeTab, url, domain);
383
+ this.tabs = tabs;
384
+ this.activeTab = activeTab;
385
+ messageBus.emit('state', { type: 'dirty' });
386
+ }
387
+ openFilesTab(view) {
388
+ const { tabs, activeTab } = addFilesTab(this.tabs, this.activeTab, view);
389
+ this.tabs = tabs;
390
+ this.activeTab = activeTab;
391
+ messageBus.emit('state', { type: 'dirty' });
392
+ }
393
+ rehydrate(loadTranscript, onState) {
394
+ const states = listAgentStates().toSorted((a, b) => (a.number ?? Infinity) - (b.number ?? Infinity));
395
+ if (states.length === 0)
396
+ return;
397
+ this.tabs = states.map((s, index) => {
398
+ const log = this.capLog(loadTranscript(s.name) ?? s.log ?? []);
399
+ const tab = makeTab(s.name, s.dotColor || distinctColor([]), s.number ?? index + 1, s.cmdHistory ?? [], log, s.workspaceDir, s.group ?? 1, s.groupColor || s.dotColor || '#5b9cff');
400
+ tab.toolStepsExpanded = false;
401
+ if (s.title)
402
+ tab.title = s.title;
403
+ if (s.offline)
404
+ tab.offline = s.offline;
405
+ return tab;
406
+ });
407
+ for (const s of states) {
408
+ if (s.cwd)
409
+ this.cwd.set(s.name, s.cwd);
410
+ if (s.context)
411
+ this.context.set(s.name, s.context);
412
+ if (s.commandQueue)
413
+ this.queue.set(s.name, s.commandQueue);
414
+ onState(s);
415
+ }
416
+ this.activeTab = 0;
417
+ }
418
+ }
@@ -0,0 +1,55 @@
1
+ export function stripComments(command) {
2
+ return command.replaceAll(/\s*##(?:[\s\S]*?##\s*|[\s\S]*)/g, ' ').trim();
3
+ }
4
+ export function renumberTabs(tabs) {
5
+ return tabs.map((t, index) => ({ ...t, number: index + 1 }));
6
+ }
7
+ export function canMoveTab(tabs, index, direction) {
8
+ const index_ = index + direction;
9
+ if (index < 0 || index_ < 0 || index_ >= tabs.length)
10
+ return false;
11
+ return tabs[index].group === tabs[index_].group;
12
+ }
13
+ export function swapTabsLeft(tabs, index) {
14
+ if (!canMoveTab(tabs, index, -1))
15
+ return tabs;
16
+ const next = [...tabs];
17
+ const left = next[index - 1];
18
+ next[index - 1] = next[index];
19
+ next[index] = left;
20
+ return renumberTabs(next);
21
+ }
22
+ export function swapTabsRight(tabs, index) {
23
+ if (!canMoveTab(tabs, index, 1))
24
+ return tabs;
25
+ const next = [...tabs];
26
+ const right = next[index + 1];
27
+ next[index + 1] = next[index];
28
+ next[index] = right;
29
+ return renumberTabs(next);
30
+ }
31
+ export function insertTabInGroup(tabs, tab, position = 'end') {
32
+ let insertAt = tabs.length;
33
+ for (const [index, tab_] of tabs.entries()) {
34
+ if (tab_.group !== tab.group)
35
+ continue;
36
+ if (position === 'start') {
37
+ insertAt = index;
38
+ break;
39
+ }
40
+ insertAt = index + 1;
41
+ }
42
+ return renumberTabs([...tabs.slice(0, insertAt), tab, ...tabs.slice(insertAt)]);
43
+ }
44
+ // A unique tab label derived from `base` (a harness name, an ssh destination's host, or a custom
45
+ // `as <label>`): `claude`, `claude-2`, … Shared by HarnessManager and SshManager so several tabs
46
+ // with the same base name can coexist.
47
+ export function uniqueLabel(tabs, base) {
48
+ const used = new Set(tabs.map((t) => t.label));
49
+ if (!used.has(base))
50
+ return base;
51
+ let n = 2;
52
+ while (used.has(`${base}-${n}`))
53
+ n++;
54
+ return `${base}-${n}`;
55
+ }