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.
- package/LICENSE +75 -0
- package/README.md +28 -0
- package/agent-names.json +52 -0
- package/bin/janus.mjs +27 -0
- package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
- package/chrome-extension/content-script.js +62 -0
- package/chrome-extension/manifest.json +25 -0
- package/chrome-extension/rules.json +16 -0
- package/dist/acp/index.js +93 -0
- package/dist/acp/loop.js +75 -0
- package/dist/acp/manager.js +126 -0
- package/dist/acp/rate-limit.js +8 -0
- package/dist/acp/runner.js +18 -0
- package/dist/acp/tools.js +27 -0
- package/dist/acp-loop.js +75 -0
- package/dist/acp-manager.js +109 -0
- package/dist/acp-runner.js +18 -0
- package/dist/acp.js +91 -0
- package/dist/agent/commands.js +29 -0
- package/dist/agent/communication-manager.js +71 -0
- package/dist/agent/state.js +58 -0
- package/dist/agent-commands.js +29 -0
- package/dist/agent-communication-manager.js +71 -0
- package/dist/agent-state.js +58 -0
- package/dist/app-themes.js +13 -0
- package/dist/browser/command.js +104 -0
- package/dist/browser/index.js +124 -0
- package/dist/browser/tab-helpers.js +51 -0
- package/dist/browser/tab.js +99 -0
- package/dist/browser-command.js +104 -0
- package/dist/browser-tab-helpers.js +51 -0
- package/dist/browser-tab.js +99 -0
- package/dist/browser.js +124 -0
- package/dist/buffer.js +42 -0
- package/dist/bus.js +52 -0
- package/dist/capture/manager.js +48 -0
- package/dist/capture/router.js +18 -0
- package/dist/capture-manager.js +48 -0
- package/dist/capture-router.js +20 -0
- package/dist/chrome-extension-loader.js +62 -0
- package/dist/cli-args.js +80 -0
- package/dist/command-manager.js +126 -0
- package/dist/command-router.js +14 -0
- package/dist/commands/acp-reset.js +13 -0
- package/dist/commands/acp.js +5 -0
- package/dist/commands/agent.js +5 -0
- package/dist/commands/broadcast.js +22 -0
- package/dist/commands/browser.js +5 -0
- package/dist/commands/clear.js +5 -0
- package/dist/commands/close.js +42 -0
- package/dist/commands/connection.js +5 -0
- package/dist/commands/db.js +5 -0
- package/dist/commands/edit.js +18 -0
- package/dist/commands/files.js +13 -0
- package/dist/commands/hist.js +7 -0
- package/dist/commands/index.js +62 -0
- package/dist/commands/monitor.js +61 -0
- package/dist/commands/msg.js +17 -0
- package/dist/commands/next.js +7 -0
- package/dist/commands/notifications.js +16 -0
- package/dist/commands/notify.js +20 -0
- package/dist/commands/open.js +41 -0
- package/dist/commands/profile.js +5 -0
- package/dist/commands/queue.js +37 -0
- package/dist/commands/quit.js +6 -0
- package/dist/commands/rename.js +12 -0
- package/dist/commands/resolve-target.js +12 -0
- package/dist/commands/schedule.js +74 -0
- package/dist/commands/search.js +26 -0
- package/dist/commands/send.js +51 -0
- package/dist/commands/state.js +9 -0
- package/dist/commands/syntax.js +36 -0
- package/dist/commands/tasks.js +7 -0
- package/dist/commands/theme.js +41 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands.js +70 -0
- package/dist/completion/browser.js +24 -0
- package/dist/completion/fs.js +26 -0
- package/dist/completion/handlers.js +81 -0
- package/dist/completion/helpers.js +43 -0
- package/dist/completion/index.js +43 -0
- package/dist/completion-browser.js +24 -0
- package/dist/completion-fs.js +26 -0
- package/dist/completion-handlers.js +69 -0
- package/dist/completion-helpers.js +43 -0
- package/dist/completion.js +42 -0
- package/dist/config.js +66 -0
- package/dist/connection/close.js +41 -0
- package/dist/connection/manager.js +93 -0
- package/dist/connection/parsing.js +28 -0
- package/dist/connection-close.js +41 -0
- package/dist/connection-manager.js +93 -0
- package/dist/connection-parsing.js +28 -0
- package/dist/connections.js +75 -0
- package/dist/controller-events.js +34 -0
- package/dist/controller-file-tree.js +36 -0
- package/dist/controller.js +201 -0
- package/dist/database/index.js +83 -0
- package/dist/database/manager.js +67 -0
- package/dist/database/parsing.js +54 -0
- package/dist/database/query.js +41 -0
- package/dist/database-manager.js +67 -0
- package/dist/database-parsing.js +54 -0
- package/dist/database-query.js +41 -0
- package/dist/database.js +83 -0
- package/dist/datetime.js +15 -0
- package/dist/editor/save.js +27 -0
- package/dist/editor/sync.js +10 -0
- package/dist/editor/watch-manager.js +84 -0
- package/dist/editor-save.js +24 -0
- package/dist/editor-watch-manager.js +84 -0
- package/dist/file-tree-args.js +36 -0
- package/dist/file-tree-git-refresh.js +33 -0
- package/dist/file-tree-manager.js +252 -0
- package/dist/file-tree-moves.js +26 -0
- package/dist/file-tree-open.js +56 -0
- package/dist/file-tree-rebuild.js +22 -0
- package/dist/file-tree-watch.js +27 -0
- package/dist/file-tree.js +82 -0
- package/dist/git-status.js +64 -0
- package/dist/github-token.js +19 -0
- package/dist/global-history.js +48 -0
- package/dist/harness/auto-approve.js +79 -0
- package/dist/harness/busy-status.js +91 -0
- package/dist/harness/capture-file.js +28 -0
- package/dist/harness/index.js +87 -0
- package/dist/harness/manager.js +182 -0
- package/dist/harness/models.js +8 -0
- package/dist/harness/recorder.js +89 -0
- package/dist/harness/recording-file.js +25 -0
- package/dist/harness/screen.js +79 -0
- package/dist/harness-capture-file.js +28 -0
- package/dist/harness-manager.js +112 -0
- package/dist/harness-models.js +8 -0
- package/dist/harness-screen.js +72 -0
- package/dist/harness.js +54 -0
- package/dist/index.js +163 -0
- package/dist/instance-lock.js +34 -0
- package/dist/interactive.js +33 -0
- package/dist/main.js +203 -0
- package/dist/managers.js +1 -0
- package/dist/message-handler.js +134 -0
- package/dist/messaging.js +47 -0
- package/dist/monitor/acp.js +24 -0
- package/dist/monitor/ask.js +31 -0
- package/dist/monitor/context.js +27 -0
- package/dist/monitor/editor-feed.js +49 -0
- package/dist/monitor/feed-diff.js +22 -0
- package/dist/monitor/feeds.js +19 -0
- package/dist/monitor/framing.js +29 -0
- package/dist/monitor/harness-feed.js +21 -0
- package/dist/monitor/info.js +29 -0
- package/dist/monitor/manager.js +221 -0
- package/dist/monitor/page-feed.js +21 -0
- package/dist/monitor/parsing.js +82 -0
- package/dist/monitor/reply.js +11 -0
- package/dist/monitor/session.js +36 -0
- package/dist/monitor/stop.js +33 -0
- package/dist/monitor/suggestion.js +12 -0
- package/dist/monitor/targets.js +69 -0
- package/dist/monitor/window.js +109 -0
- package/dist/monitor-acp.js +22 -0
- package/dist/monitor-info.js +18 -0
- package/dist/monitor-manager.js +233 -0
- package/dist/monitor-parsing.js +68 -0
- package/dist/monitor-session.js +19 -0
- package/dist/monitor-targets.js +37 -0
- package/dist/monitor-window.js +81 -0
- package/dist/notifications-tab.js +32 -0
- package/dist/notifications.js +87 -0
- package/dist/open-file-manager.js +100 -0
- package/dist/openers/editor.js +41 -0
- package/dist/openers/external-viewer.js +11 -0
- package/dist/openers/image.js +27 -0
- package/dist/openers/index.js +16 -0
- package/dist/openers/markdown.js +20 -0
- package/dist/openers/os-open.js +24 -0
- package/dist/openers/page.js +51 -0
- package/dist/openers/size.js +13 -0
- package/dist/openers/types.js +1 -0
- package/dist/page/sync.js +10 -0
- package/dist/paths.js +45 -0
- package/dist/persona-parsing.js +49 -0
- package/dist/personas.js +44 -0
- package/dist/profile/agent-opener.js +154 -0
- package/dist/profile/files.js +17 -0
- package/dist/profile/manager.js +72 -0
- package/dist/profile/monitors.js +23 -0
- package/dist/profile/notifications.js +11 -0
- package/dist/profile-agent-opener.js +136 -0
- package/dist/profile-manager.js +72 -0
- package/dist/profiles.js +143 -0
- package/dist/protocol.js +1 -0
- package/dist/pseudoterminal-manager.js +106 -0
- package/dist/pty.js +41 -0
- package/dist/recognizers/acp.js +38 -0
- package/dist/recognizers/analyze.js +49 -0
- package/dist/recognizers/bash.js +51 -0
- package/dist/recognizers/db.js +26 -0
- package/dist/recognizers/index.js +1 -0
- package/dist/recognizers/lexicon.js +11 -0
- package/dist/recognizers/types.js +5 -0
- package/dist/resolve.js +39 -0
- package/dist/route-choice.js +11 -0
- package/dist/sandbox/index.js +217 -0
- package/dist/sandbox/paths.js +178 -0
- package/dist/sandbox/profile.js +161 -0
- package/dist/sandbox-paths.js +159 -0
- package/dist/sandbox-profile.js +161 -0
- package/dist/sandbox.js +217 -0
- package/dist/schedule/display.js +19 -0
- package/dist/schedule/helpers.js +76 -0
- package/dist/schedule/index.js +76 -0
- package/dist/schedule/manager.js +101 -0
- package/dist/schedule/parsing.js +63 -0
- package/dist/schedule/time.js +34 -0
- package/dist/schedule-display.js +19 -0
- package/dist/schedule-helpers.js +76 -0
- package/dist/schedule-manager.js +98 -0
- package/dist/schedule-parsing.js +63 -0
- package/dist/schedule-time.js +34 -0
- package/dist/schedule.js +76 -0
- package/dist/search-matches.js +40 -0
- package/dist/security.js +44 -0
- package/dist/shell-manager.js +139 -0
- package/dist/shell.js +59 -0
- package/dist/ssh-manager.js +42 -0
- package/dist/ssh.js +39 -0
- package/dist/startup-errors.js +21 -0
- package/dist/state-event.js +14 -0
- package/dist/state-format.js +38 -0
- package/dist/syntax-themes.js +14 -0
- package/dist/tab/agent-state.js +19 -0
- package/dist/tab/cleanup.js +33 -0
- package/dist/tab/colors.js +43 -0
- package/dist/tab/creators.js +95 -0
- package/dist/tab/dock.js +35 -0
- package/dist/tab/focus-history.js +41 -0
- package/dist/tab/formatting-handlers.js +77 -0
- package/dist/tab/formatting.js +21 -0
- package/dist/tab/history.js +14 -0
- package/dist/tab/index.js +65 -0
- package/dist/tab/manager.js +329 -0
- package/dist/tab/openers.js +35 -0
- package/dist/tab/queue.js +33 -0
- package/dist/tab/rehydrate.js +15 -0
- package/dist/tab/reorder.js +15 -0
- package/dist/tab/transcript.js +15 -0
- package/dist/tab/utils.js +55 -0
- package/dist/tab/view.js +42 -0
- package/dist/tab-cleanup.js +27 -0
- package/dist/tab-colors.js +43 -0
- package/dist/tab-creators.js +85 -0
- package/dist/tab-formatting-handlers.js +77 -0
- package/dist/tab-formatting.js +21 -0
- package/dist/tab-manager.js +418 -0
- package/dist/tab-utils.js +55 -0
- package/dist/tab.js +57 -0
- package/dist/tasks.js +32 -0
- package/dist/transcript/logger.js +36 -0
- package/dist/transcript/store.js +66 -0
- package/dist/types.js +6 -0
- package/dist/user-agent.js +65 -0
- package/dist/word-wrapping.js +42 -0
- package/dist/workspace-manager.js +40 -0
- package/dist/workspace.js +120 -0
- package/help.md +97 -0
- package/package.json +138 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { makeTab, distinctColor, insertTabInGroup, } from './index.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 './cleanup.js';
|
|
7
|
+
import * as tabOpeners from './openers.js';
|
|
8
|
+
import { getQueue, pushQueue, shiftQueue, updateQueueEntry, removeQueueEntry } from './queue.js';
|
|
9
|
+
import { buildTabView } from './view.js';
|
|
10
|
+
import { rehydrateTabs } from './rehydrate.js';
|
|
11
|
+
import { buildAgentStateFromTab } from './agent-state.js';
|
|
12
|
+
import { recordLeavingActiveTab, popFocusHistory, mostRecentFileTreeLabel } from './focus-history.js';
|
|
13
|
+
import { applyDock } from './dock.js';
|
|
14
|
+
import { capLog, finishRunningEntry } from './transcript.js';
|
|
15
|
+
import { computeReorder, removeTabAt } from './reorder.js';
|
|
16
|
+
import { recordHistory } from './history.js';
|
|
17
|
+
export class TabManager {
|
|
18
|
+
managers;
|
|
19
|
+
tabs = [];
|
|
20
|
+
activeTab = 0;
|
|
21
|
+
cwd = new Map();
|
|
22
|
+
busy = new Set();
|
|
23
|
+
context = new Map();
|
|
24
|
+
queue = new Map();
|
|
25
|
+
onIdle = null;
|
|
26
|
+
openFiles = new Map();
|
|
27
|
+
openFileCounter = 0;
|
|
28
|
+
// Labels of tabs that were previously active, most-recent-last. Closing the active tab pops
|
|
29
|
+
// this to restore focus to whatever was focused right before it, rather than just clamping to
|
|
30
|
+
// the nearest surviving index.
|
|
31
|
+
focusHistory = [];
|
|
32
|
+
rootDir;
|
|
33
|
+
get launchDir() { return this.rootDir; }
|
|
34
|
+
static OPEN_MAX_FILES = 10;
|
|
35
|
+
constructor(managers, projectDir) {
|
|
36
|
+
this.managers = managers;
|
|
37
|
+
this.rootDir = projectDir ?? process.cwd();
|
|
38
|
+
this.tabs = [this.makeRootTab()];
|
|
39
|
+
this.cwd.set('janus', this.rootDir);
|
|
40
|
+
}
|
|
41
|
+
cur() {
|
|
42
|
+
return this.tabs[this.activeTab] ?? this.tabs[0];
|
|
43
|
+
}
|
|
44
|
+
allLabels() {
|
|
45
|
+
return this.tabs.map((t) => t.label);
|
|
46
|
+
}
|
|
47
|
+
isBusy(label) {
|
|
48
|
+
return this.busy.has(label);
|
|
49
|
+
}
|
|
50
|
+
cwdOf(label) {
|
|
51
|
+
return this.cwd.get(label);
|
|
52
|
+
}
|
|
53
|
+
setCwd(label, dir) {
|
|
54
|
+
this.cwd.set(label, dir);
|
|
55
|
+
}
|
|
56
|
+
addBusy(label) {
|
|
57
|
+
this.busy.add(label);
|
|
58
|
+
}
|
|
59
|
+
deleteBusy(label) {
|
|
60
|
+
this.busy.delete(label);
|
|
61
|
+
if (this.queueFor(label).length > 0) {
|
|
62
|
+
queueMicrotask(() => this.onIdle?.(label));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
setOnIdle(hook) {
|
|
66
|
+
this.onIdle = hook;
|
|
67
|
+
}
|
|
68
|
+
queueFor(label) {
|
|
69
|
+
return getQueue(this.queue, label);
|
|
70
|
+
}
|
|
71
|
+
enqueue(label, text) {
|
|
72
|
+
pushQueue(this.queue, label, text);
|
|
73
|
+
this.persistQueue(label);
|
|
74
|
+
}
|
|
75
|
+
dequeue(label) {
|
|
76
|
+
const front = shiftQueue(this.queue, label);
|
|
77
|
+
if (front !== undefined)
|
|
78
|
+
this.persistQueue(label);
|
|
79
|
+
return front;
|
|
80
|
+
}
|
|
81
|
+
editQueued(label, index, text) {
|
|
82
|
+
if (updateQueueEntry(this.queue, label, index, text))
|
|
83
|
+
this.persistQueue(label);
|
|
84
|
+
}
|
|
85
|
+
deleteQueued(label, index) {
|
|
86
|
+
if (removeQueueEntry(this.queue, label, index))
|
|
87
|
+
this.persistQueue(label);
|
|
88
|
+
}
|
|
89
|
+
persistQueue(label) {
|
|
90
|
+
const tab = this.tabs.find((t) => t.label === label);
|
|
91
|
+
if (tab)
|
|
92
|
+
this.persist(this.buildAgentState(tab));
|
|
93
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
94
|
+
}
|
|
95
|
+
contextFor(label) {
|
|
96
|
+
return this.context.get(label) ?? [];
|
|
97
|
+
}
|
|
98
|
+
setContext(label, ctx) {
|
|
99
|
+
this.context.set(label, ctx);
|
|
100
|
+
}
|
|
101
|
+
appendContext(label, text) {
|
|
102
|
+
this.context.set(label, [...(this.context.get(label) ?? []), text]);
|
|
103
|
+
}
|
|
104
|
+
findIndex(label) {
|
|
105
|
+
return this.tabs.findIndex((t) => t.label === label);
|
|
106
|
+
}
|
|
107
|
+
persist(state) {
|
|
108
|
+
try {
|
|
109
|
+
saveAgentState(state);
|
|
110
|
+
}
|
|
111
|
+
catch { /* ignore */ }
|
|
112
|
+
}
|
|
113
|
+
buildAgentState(tab, extra) {
|
|
114
|
+
return buildAgentStateFromTab(tab, this.busy.has(tab.label), this.cwd.get(tab.label), this.context.get(tab.label), this.queue.get(tab.label), extra);
|
|
115
|
+
}
|
|
116
|
+
activeLabel() {
|
|
117
|
+
return this.tabs[this.activeTab]?.label;
|
|
118
|
+
}
|
|
119
|
+
markUnread(label) {
|
|
120
|
+
if (label === this.activeLabel())
|
|
121
|
+
return;
|
|
122
|
+
const tab = this.tabs.find((t) => t.label === label);
|
|
123
|
+
if (tab)
|
|
124
|
+
tab.hasUnread = true;
|
|
125
|
+
}
|
|
126
|
+
recordLeavingActiveTab(newIndex) {
|
|
127
|
+
this.focusHistory = recordLeavingActiveTab(this.tabs, this.activeTab, this.focusHistory, newIndex);
|
|
128
|
+
}
|
|
129
|
+
popFocusHistory() {
|
|
130
|
+
const { index, history } = popFocusHistory(this.tabs, this.focusHistory);
|
|
131
|
+
this.focusHistory = history;
|
|
132
|
+
return index;
|
|
133
|
+
}
|
|
134
|
+
mostRecentFileTreeLabel() {
|
|
135
|
+
return mostRecentFileTreeLabel(this.tabs, this.focusHistory);
|
|
136
|
+
}
|
|
137
|
+
// Applies the result of adding a new tab (or focusing an existing one) from the `openers.ts`
|
|
138
|
+
// helpers, which otherwise assign `tabs`/`activeTab` directly and would bypass focus-history
|
|
139
|
+
// tracking — a freshly opened, auto-focused tab still needs its predecessor recorded.
|
|
140
|
+
applyOpenResult(result) {
|
|
141
|
+
this.recordLeavingActiveTab(result.activeTab);
|
|
142
|
+
this.tabs = result.tabs;
|
|
143
|
+
this.activeTab = result.activeTab;
|
|
144
|
+
}
|
|
145
|
+
setActiveTab(index) {
|
|
146
|
+
if (index < 0 || index >= this.tabs.length)
|
|
147
|
+
return;
|
|
148
|
+
if (this.tabs[index]?.dock)
|
|
149
|
+
return; // a docked tab is never the active tab
|
|
150
|
+
this.recordLeavingActiveTab(index);
|
|
151
|
+
this.activeTab = index;
|
|
152
|
+
const tab = this.tabs[index];
|
|
153
|
+
if (tab)
|
|
154
|
+
tab.hasUnread = false;
|
|
155
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
156
|
+
}
|
|
157
|
+
moveTab(dir) {
|
|
158
|
+
const total = this.tabs.length;
|
|
159
|
+
for (let step = 1; step <= total; step++) {
|
|
160
|
+
const index = (this.activeTab + dir * step + total) % total;
|
|
161
|
+
if (!this.tabs[index]?.dock) {
|
|
162
|
+
this.setActiveTab(index);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
setDock(index, dock) {
|
|
168
|
+
if (this.tabs[index] === undefined)
|
|
169
|
+
return;
|
|
170
|
+
this.activeTab = applyDock(this.tabs, this.activeTab, index, dock, (i) => this.recordLeavingActiveTab(i));
|
|
171
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
172
|
+
}
|
|
173
|
+
reorderTab(dir) {
|
|
174
|
+
const from = this.activeTab;
|
|
175
|
+
const result = computeReorder(this.tabs, from, dir);
|
|
176
|
+
if (!result)
|
|
177
|
+
return;
|
|
178
|
+
this.tabs = result.tabs;
|
|
179
|
+
this.activeTab = result.activeTab;
|
|
180
|
+
const active = this.tabs[this.activeTab];
|
|
181
|
+
if (active)
|
|
182
|
+
active.hasUnread = false;
|
|
183
|
+
this.persist(this.buildAgentState(this.tabs[from]));
|
|
184
|
+
this.persist(this.buildAgentState(this.tabs[this.activeTab]));
|
|
185
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
186
|
+
}
|
|
187
|
+
closeTab(index) {
|
|
188
|
+
const tab = this.tabs[index];
|
|
189
|
+
if (!tab)
|
|
190
|
+
return;
|
|
191
|
+
const nonDockedCount = this.tabs.filter((t) => !t.dock).length;
|
|
192
|
+
closeTabResources(tab, this.managers, this.openFiles, this.context, this.queue, nonDockedCount);
|
|
193
|
+
// Closing the last remaining non-docked tab quits the app (same as the `quit` command).
|
|
194
|
+
if (!tab.dock && nonDockedCount <= 1) {
|
|
195
|
+
messageBus.emit('app', { type: 'exit' });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const wasActive = index === this.activeTab;
|
|
199
|
+
this.focusHistory = this.focusHistory.filter((l) => l !== tab.label);
|
|
200
|
+
this.tabs = removeTabAt(this.tabs, index);
|
|
201
|
+
const restored = wasActive ? this.popFocusHistory() : undefined;
|
|
202
|
+
this.activeTab = restored ?? Math.min(this.activeTab, this.tabs.length - 1);
|
|
203
|
+
const active = this.tabs[this.activeTab];
|
|
204
|
+
if (active)
|
|
205
|
+
active.hasUnread = false;
|
|
206
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
207
|
+
}
|
|
208
|
+
renameTab(index, title) {
|
|
209
|
+
const tab = this.tabs[index];
|
|
210
|
+
if (!tab)
|
|
211
|
+
return;
|
|
212
|
+
const trimmed = title.trim().slice(0, getConfig().tabNameMaxLength);
|
|
213
|
+
if (trimmed && trimmed !== tab.label)
|
|
214
|
+
tab.title = trimmed;
|
|
215
|
+
else
|
|
216
|
+
delete tab.title;
|
|
217
|
+
this.persist(this.buildAgentState(tab));
|
|
218
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
219
|
+
}
|
|
220
|
+
toggleCollapse() {
|
|
221
|
+
const tab = this.cur();
|
|
222
|
+
tab.toolStepsExpanded = !tab.toolStepsExpanded;
|
|
223
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
224
|
+
}
|
|
225
|
+
insertTabInGroup(tab) {
|
|
226
|
+
this.tabs = insertTabInGroup(this.tabs, tab);
|
|
227
|
+
}
|
|
228
|
+
makeRootTab() {
|
|
229
|
+
const tab = makeTab('janus', distinctColor([]));
|
|
230
|
+
tab.toolStepsExpanded = false;
|
|
231
|
+
return tab;
|
|
232
|
+
}
|
|
233
|
+
startRunning(label, input) {
|
|
234
|
+
this.busy.add(label);
|
|
235
|
+
this.append(label, { input, output: '', running: true });
|
|
236
|
+
}
|
|
237
|
+
finishRunning(label, output) {
|
|
238
|
+
const t = this.tabs.find((x) => x.label === label);
|
|
239
|
+
if (t) {
|
|
240
|
+
t.log = finishRunningEntry(t.log, output);
|
|
241
|
+
this.deleteBusy(label);
|
|
242
|
+
this.persist(this.buildAgentState(t));
|
|
243
|
+
}
|
|
244
|
+
if (output && t) {
|
|
245
|
+
messageBus.emit('transcript', {
|
|
246
|
+
type: 'entry:appended', tabLabel: label, entry: { input: '', output }, tab: t,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
this.markUnread(label);
|
|
250
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
251
|
+
}
|
|
252
|
+
capLog(log) {
|
|
253
|
+
return capLog(log, getConfig().transcriptMaxLines);
|
|
254
|
+
}
|
|
255
|
+
append(label, entry) {
|
|
256
|
+
const tab = this.tabs.find((t) => t.label === label);
|
|
257
|
+
if (!tab)
|
|
258
|
+
return;
|
|
259
|
+
const before = tab.log.length;
|
|
260
|
+
tab.log = this.capLog([...tab.log, entry]);
|
|
261
|
+
tab.scrollOffset = 0;
|
|
262
|
+
const trimmed = before + 1 - tab.log.length;
|
|
263
|
+
if (trimmed > 0)
|
|
264
|
+
messageBus.emit('transcript', { type: 'entries:trimmed', tabLabel: label, count: trimmed });
|
|
265
|
+
messageBus.emit('transcript', { type: 'entry:appended', tabLabel: label, entry, tab });
|
|
266
|
+
this.markUnread(label);
|
|
267
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
268
|
+
}
|
|
269
|
+
clearTranscript(label) {
|
|
270
|
+
const tab = this.tabs.find((t) => t.label === label);
|
|
271
|
+
if (!tab)
|
|
272
|
+
return;
|
|
273
|
+
tab.log = [];
|
|
274
|
+
this.persist(this.buildAgentState(tab));
|
|
275
|
+
messageBus.emit('transcript', { type: 'tab:cleared', tabLabel: label });
|
|
276
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
277
|
+
}
|
|
278
|
+
recordHistory(index, text) {
|
|
279
|
+
return recordHistory(this.tabs[index], text);
|
|
280
|
+
}
|
|
281
|
+
shorten(p) {
|
|
282
|
+
return abbreviatePath(p, { root: this.rootDir });
|
|
283
|
+
}
|
|
284
|
+
registerFile(absPath) {
|
|
285
|
+
const id = String(++this.openFileCounter);
|
|
286
|
+
this.openFiles.set(id, absPath);
|
|
287
|
+
return `/open/${id}`;
|
|
288
|
+
}
|
|
289
|
+
openFilePath(id) {
|
|
290
|
+
return this.openFiles.get(id);
|
|
291
|
+
}
|
|
292
|
+
view(connectionsFor, acpLabel, scheduleView) {
|
|
293
|
+
return this.tabs.map((t) => buildTabView(t, this.busy.has(t.label), this.cwd.get(t.label) ?? process.cwd(), acpLabel(t.label), connectionsFor(t.label), scheduleView(t.label), this.queue.get(t.label) ?? [], (p) => this.shorten(p)));
|
|
294
|
+
}
|
|
295
|
+
openImageTab(image) {
|
|
296
|
+
tabOpeners.openImageTab(this, image);
|
|
297
|
+
}
|
|
298
|
+
openMarkdownTab(view) {
|
|
299
|
+
tabOpeners.openMarkdownTab(this, view);
|
|
300
|
+
}
|
|
301
|
+
openEditorTab(view) {
|
|
302
|
+
tabOpeners.openEditorTab(this, view, (label, path) => this.managers.editorWatch.watch(label, path));
|
|
303
|
+
}
|
|
304
|
+
openPageTab(view) {
|
|
305
|
+
tabOpeners.openPageTab(this, view);
|
|
306
|
+
}
|
|
307
|
+
openFilesTab(view) {
|
|
308
|
+
tabOpeners.openFilesTab(this, view);
|
|
309
|
+
}
|
|
310
|
+
openNotificationsTab() {
|
|
311
|
+
tabOpeners.openNotificationsTab(this);
|
|
312
|
+
}
|
|
313
|
+
rehydrate(loadTranscript, onState) {
|
|
314
|
+
const states = listAgentStates().toSorted((a, b) => (a.number ?? Infinity) - (b.number ?? Infinity));
|
|
315
|
+
if (states.length === 0)
|
|
316
|
+
return;
|
|
317
|
+
this.tabs = rehydrateTabs(states, loadTranscript, (log) => this.capLog(log));
|
|
318
|
+
for (const s of states) {
|
|
319
|
+
if (s.cwd)
|
|
320
|
+
this.cwd.set(s.name, s.cwd);
|
|
321
|
+
if (s.context)
|
|
322
|
+
this.context.set(s.name, s.context);
|
|
323
|
+
if (s.commandQueue)
|
|
324
|
+
this.queue.set(s.name, s.commandQueue);
|
|
325
|
+
onState(s);
|
|
326
|
+
}
|
|
327
|
+
this.activeTab = 0;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { messageBus } from '../bus.js';
|
|
2
|
+
import { addImageTab, addMarkdownTab, addEditorTab, addPageTab, addFilesTab, addNotificationsTab, } from './creators.js';
|
|
3
|
+
function activate(target, result) {
|
|
4
|
+
target.applyOpenResult(result);
|
|
5
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
6
|
+
}
|
|
7
|
+
export function openImageTab(target, image) {
|
|
8
|
+
activate(target, addImageTab(target.tabs, target.activeTab, image));
|
|
9
|
+
}
|
|
10
|
+
export function openMarkdownTab(target, view) {
|
|
11
|
+
activate(target, addMarkdownTab(target.tabs, target.activeTab, view));
|
|
12
|
+
}
|
|
13
|
+
export function openEditorTab(target, view, watch) {
|
|
14
|
+
const existing = target.tabs.find((t) => t.editor?.path === view.path);
|
|
15
|
+
if (existing) {
|
|
16
|
+
if (view.line !== undefined)
|
|
17
|
+
existing.editor.line = view.line;
|
|
18
|
+
target.setActiveTab(target.tabs.indexOf(existing));
|
|
19
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const result = addEditorTab(target.tabs, target.activeTab, view);
|
|
23
|
+
target.applyOpenResult(result);
|
|
24
|
+
watch(result.tabs[result.activeTab].label, view.path);
|
|
25
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
26
|
+
}
|
|
27
|
+
export function openPageTab(target, { url, domain }) {
|
|
28
|
+
activate(target, addPageTab(target.tabs, target.activeTab, url, domain));
|
|
29
|
+
}
|
|
30
|
+
export function openFilesTab(target, view) {
|
|
31
|
+
activate(target, addFilesTab(target.tabs, target.activeTab, view));
|
|
32
|
+
}
|
|
33
|
+
export function openNotificationsTab(target) {
|
|
34
|
+
activate(target, addNotificationsTab(target.tabs, target.activeTab));
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Per-tab command queue: an agent's queued-but-not-yet-run commands (`queue <agent> <cmd>`),
|
|
2
|
+
// keyed by tab label. Pure operations on the queue Map itself; persistence and change
|
|
3
|
+
// notification stay the caller's responsibility (TabManager).
|
|
4
|
+
export function getQueue(queue, label) {
|
|
5
|
+
return queue.get(label) ?? [];
|
|
6
|
+
}
|
|
7
|
+
export function pushQueue(queue, label, text) {
|
|
8
|
+
queue.set(label, [...getQueue(queue, label), text]);
|
|
9
|
+
}
|
|
10
|
+
export function shiftQueue(queue, label) {
|
|
11
|
+
const q = getQueue(queue, label);
|
|
12
|
+
if (q.length === 0)
|
|
13
|
+
return undefined;
|
|
14
|
+
const [front, ...rest] = q;
|
|
15
|
+
queue.set(label, rest);
|
|
16
|
+
return front;
|
|
17
|
+
}
|
|
18
|
+
export function updateQueueEntry(queue, label, index, text) {
|
|
19
|
+
const q = getQueue(queue, label);
|
|
20
|
+
if (index < 0 || index >= q.length)
|
|
21
|
+
return false;
|
|
22
|
+
const next = [...q];
|
|
23
|
+
next[index] = text;
|
|
24
|
+
queue.set(label, next);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
export function removeQueueEntry(queue, label, index) {
|
|
28
|
+
const q = getQueue(queue, label);
|
|
29
|
+
if (index < 0 || index >= q.length)
|
|
30
|
+
return false;
|
|
31
|
+
queue.set(label, q.filter((_, i) => i !== index));
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { makeTab, distinctColor } from './index.js';
|
|
2
|
+
// Reconstructs Tab objects (in `states`' order, e.g. sorted by persisted number) from saved
|
|
3
|
+
// AgentState records, restoring each tab's transcript, history, title, and offline flag.
|
|
4
|
+
export function rehydrateTabs(states, loadTranscript, capLog) {
|
|
5
|
+
return states.map((s, index) => {
|
|
6
|
+
const log = capLog(loadTranscript(s.name) ?? s.log ?? []);
|
|
7
|
+
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');
|
|
8
|
+
tab.toolStepsExpanded = false;
|
|
9
|
+
if (s.title)
|
|
10
|
+
tab.title = s.title;
|
|
11
|
+
if (s.offline)
|
|
12
|
+
tab.offline = s.offline;
|
|
13
|
+
return tab;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { swapTabsLeft, swapTabsRight } from './index.js';
|
|
2
|
+
// Resolves TabManager.reorderTab: swaps the active tab with its left/right neighbor (skipping
|
|
3
|
+
// docked tabs, per swapTabsLeft/swapTabsRight) and moves `activeTab` along with it. Returns
|
|
4
|
+
// undefined when the swap is a no-op (already at an edge).
|
|
5
|
+
export function computeReorder(tabs, activeTab, dir) {
|
|
6
|
+
const next = dir < 0 ? swapTabsLeft(tabs, activeTab) : swapTabsRight(tabs, activeTab);
|
|
7
|
+
if (next === tabs)
|
|
8
|
+
return undefined;
|
|
9
|
+
const to = dir < 0 ? Math.max(0, activeTab - 1) : Math.min(activeTab + 1, next.length - 1);
|
|
10
|
+
return { tabs: next, activeTab: to };
|
|
11
|
+
}
|
|
12
|
+
// Removes the tab at `index`, renumbering the survivors (TabManager.closeTab).
|
|
13
|
+
export function removeTabAt(tabs, index) {
|
|
14
|
+
return tabs.filter((_, i) => i !== index).map((t, i) => ({ ...t, number: i + 1 }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Caps a log to at most `max` entries, dropping the oldest. Shared by every mutation that grows a
|
|
2
|
+
// tab's log (append) or resumes one rehydrated from disk (TabManager.rehydrate).
|
|
3
|
+
export function capLog(log, max) {
|
|
4
|
+
return log.length > max ? log.slice(log.length - max) : log;
|
|
5
|
+
}
|
|
6
|
+
// Marks the most recent still-running log entry (if any) as finished with `output`. Returns the
|
|
7
|
+
// same log reference when nothing was running.
|
|
8
|
+
export function finishRunningEntry(log, output) {
|
|
9
|
+
const index = log.findLastIndex((e) => e.running);
|
|
10
|
+
if (index === -1)
|
|
11
|
+
return log;
|
|
12
|
+
const next = [...log];
|
|
13
|
+
next[index] = { ...next[index], output, running: false };
|
|
14
|
+
return next;
|
|
15
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/tab/view.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { flattenBuffer } from './index.js';
|
|
2
|
+
// Converts one internal Tab into the wire-format TabView sent to the client — the shape the
|
|
3
|
+
// client actually renders, as opposed to Tab's server-side bookkeeping fields.
|
|
4
|
+
export function buildTabView(tab, busy, cwd, acp, connections, schedule, commandQueue, shorten) {
|
|
5
|
+
return {
|
|
6
|
+
label: tab.label,
|
|
7
|
+
number: tab.number,
|
|
8
|
+
dotColor: tab.dotColor,
|
|
9
|
+
group: tab.group,
|
|
10
|
+
groupColor: tab.groupColor,
|
|
11
|
+
busy,
|
|
12
|
+
hasUnread: !!tab.hasUnread,
|
|
13
|
+
cwd: shorten(cwd),
|
|
14
|
+
flags: [
|
|
15
|
+
...(tab.workspaceDir ? ['workspaced'] : []),
|
|
16
|
+
...(tab.autoApprove ? ['autoApprove'] : []),
|
|
17
|
+
],
|
|
18
|
+
acp,
|
|
19
|
+
connections,
|
|
20
|
+
schedule,
|
|
21
|
+
bufferLines: flattenBuffer(tab.log, !tab.toolStepsExpanded)
|
|
22
|
+
.map((l) => (l.cwd ? { ...l, cwd: shorten(l.cwd) } : l)),
|
|
23
|
+
cmdHistory: tab.cmdHistory,
|
|
24
|
+
commandQueue,
|
|
25
|
+
toolStepsExpanded: !!tab.toolStepsExpanded,
|
|
26
|
+
view: tab.view,
|
|
27
|
+
title: tab.title,
|
|
28
|
+
image: tab.image,
|
|
29
|
+
page: tab.page,
|
|
30
|
+
harness: tab.harness,
|
|
31
|
+
markdown: tab.markdown,
|
|
32
|
+
editor: tab.editor,
|
|
33
|
+
// Deliberately NOT spreading `tab.editorDraft` here: the transient unsaved buffer is
|
|
34
|
+
// server-only and must never be broadcast back to clients (see editor-live-buffer-sync plan).
|
|
35
|
+
// Same for `tab.pageSnapshot`: the visible-text cache is server-only, read by monitor page
|
|
36
|
+
// feeds, and must never be broadcast back to clients (see monitor-page-tab-content-feed plan).
|
|
37
|
+
monitor: tab.monitor,
|
|
38
|
+
files: tab.files ? { ...tab.files, root: shorten(tab.files.root), absoluteRoot: tab.files.root } : undefined,
|
|
39
|
+
activePty: tab.activePty,
|
|
40
|
+
dock: tab.dock,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { messageBus } from './bus.js';
|
|
2
|
+
export function closeTabResources(tab, managers, openFiles, context, queue, tabsLength) {
|
|
3
|
+
if (tab.workspaceDir)
|
|
4
|
+
managers.workspace.remove(tab.workspaceDir);
|
|
5
|
+
managers.shell.close(tab.label);
|
|
6
|
+
managers.acp.close(tab.label);
|
|
7
|
+
managers.browser.closeTab(tab.label);
|
|
8
|
+
managers.pty.closeTab(tab.label);
|
|
9
|
+
managers.tab.deleteBusy(tab.label);
|
|
10
|
+
managers.fileTree.closeTab(tab.label);
|
|
11
|
+
managers.editorWatch.closeTab(tab.label);
|
|
12
|
+
managers.schedule.delete(tab.label);
|
|
13
|
+
managers.database.forgetTab(tab.label);
|
|
14
|
+
if (tabsLength <= 1)
|
|
15
|
+
managers.database.closeAll();
|
|
16
|
+
messageBus.emit('transcript', { type: 'tab:removed', tabLabel: tab.label });
|
|
17
|
+
if (tab.image) {
|
|
18
|
+
const id = tab.image.url.replace(/^\/open\//, '');
|
|
19
|
+
openFiles.delete(id);
|
|
20
|
+
}
|
|
21
|
+
if (tab.markdown) {
|
|
22
|
+
const id = tab.markdown.url.replace(/^\/open\//, '');
|
|
23
|
+
openFiles.delete(id);
|
|
24
|
+
}
|
|
25
|
+
context.delete(tab.label);
|
|
26
|
+
queue.delete(tab.label);
|
|
27
|
+
}
|
|
@@ -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
|
+
}
|