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,21 @@
|
|
|
1
|
+
import { resolveTargetTabs } from './targets.js';
|
|
2
|
+
// Turn harness-view targets into monitor buffer entries. Harness tabs have no `LogEntry` transcript,
|
|
3
|
+
// so a monitor watching one instead receives that tab's latest rendered screen (the coherent,
|
|
4
|
+
// de-ANSI'd text the screen reader already computes) as a `LogEntry` tagged with the tab label.
|
|
5
|
+
// Entries are emitted only when the capture is newer than the last one fed for that tab (deduped by
|
|
6
|
+
// `capturedAt`), so an idle harness — whose screen reader keeps returning the same capture — never
|
|
7
|
+
// re-prompts the monitor. Non-harness targets are ignored here; they flow through the tab log and
|
|
8
|
+
// the `entry:appended` channel instead.
|
|
9
|
+
export function harnessFeedEntries(managers, targets, harnessSeen) {
|
|
10
|
+
const entries = [];
|
|
11
|
+
for (const tab of resolveTargetTabs(managers.tab.tabs, targets)) {
|
|
12
|
+
if (tab.view !== 'harness')
|
|
13
|
+
continue;
|
|
14
|
+
const latest = managers.harness.latestScreenText(tab.label);
|
|
15
|
+
if (!latest || harnessSeen.get(tab.label) === latest.capturedAt)
|
|
16
|
+
continue;
|
|
17
|
+
harnessSeen.set(tab.label, latest.capturedAt);
|
|
18
|
+
entries.push({ tabLabel: tab.label, entry: { input: '', output: latest.text } });
|
|
19
|
+
}
|
|
20
|
+
return entries;
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { formatTargets } from './targets.js';
|
|
2
|
+
// Read-only projections of the live monitor registry: the `monitors` command listing
|
|
3
|
+
// and the connections-panel rows.
|
|
4
|
+
export function listMonitors(monitors) {
|
|
5
|
+
return [...monitors].map((reg) => {
|
|
6
|
+
const targets = formatTargets(reg.targets);
|
|
7
|
+
const mode = reg.inline ? 'inline' : 'external';
|
|
8
|
+
return `${reg.persona.name}: ${targets} ← ${reg.owner} (${mode}, ${reg.delivered} suggestion${reg.delivered === 1 ? '' : 's'})`;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
// Joins provider and model into a single "provider/model" label, dropping whichever is absent.
|
|
12
|
+
export function formatConnection(info) {
|
|
13
|
+
return [info.provider, info.model].filter(Boolean).join('/');
|
|
14
|
+
}
|
|
15
|
+
// A persona's body opens with a single declarative sentence (e.g. "You are a security
|
|
16
|
+
// monitor."); take up to its first period as a concise one-sentence summary.
|
|
17
|
+
export function personaSummary(persona) {
|
|
18
|
+
const period = persona.body.indexOf('.');
|
|
19
|
+
return period === -1 ? persona.body.trim() : persona.body.slice(0, period + 1);
|
|
20
|
+
}
|
|
21
|
+
// Rows for a tab's monitors (e.g. `monitor:security (opencode/…)`).
|
|
22
|
+
export function monitorConnections(monitors, owner) {
|
|
23
|
+
return [...monitors]
|
|
24
|
+
.filter((reg) => reg.owner === owner)
|
|
25
|
+
.map((reg) => {
|
|
26
|
+
const info = reg.info ? ` (${formatConnection(reg.info)})` : '';
|
|
27
|
+
return { text: `monitor:${reg.persona.name}${info}`, kind: 'acp' };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { messageBus } from '../bus.js';
|
|
2
|
+
import { loadPersona } from '../personas.js';
|
|
3
|
+
import { parseSuggestion } from './parsing.js';
|
|
4
|
+
import { openMonitorTab, pushSuggestion, rateSuggestion, updateMonitorMeta } from './window.js';
|
|
5
|
+
import { openMonitorSession, respawnMonitorSession } from './session.js';
|
|
6
|
+
import { spawnMonitorSession } from './acp.js';
|
|
7
|
+
import { validateTargets, matchesTargets, targetColor, formatTargets, resolveTargetAliases } from './targets.js';
|
|
8
|
+
import { stopMonitor, closeIfUnfed } from './stop.js';
|
|
9
|
+
import { seedFeedEntries, flushFeedEntries } from './feeds.js';
|
|
10
|
+
import { generateSessionDelimiter, frameEntry } from './framing.js';
|
|
11
|
+
import { recordContext, snapshotMonitorContext } from './context.js';
|
|
12
|
+
import { listMonitors, monitorConnections } from './info.js';
|
|
13
|
+
import { askMonitor } from './ask.js';
|
|
14
|
+
import { recordReply } from './reply.js';
|
|
15
|
+
import { notify } from '../notifications.js';
|
|
16
|
+
import { isRateLimitError } from '../acp/rate-limit.js';
|
|
17
|
+
import { SUGGESTION_PREFIX, buildSuggestion, formatInlineSuggestion } from './suggestion.js';
|
|
18
|
+
export { SUGGESTION_PREFIX } from './suggestion.js';
|
|
19
|
+
export const MONITOR_FLUSH_MS = 30_000;
|
|
20
|
+
// Owns all live monitors, keyed by `${ownerLabel}:${persona}`. Each monitor is a
|
|
21
|
+
// dedicated, tool-less ACP session primed with its persona; transcript entries from its
|
|
22
|
+
// targets buffer up and flush as one prompt every 30s (never when the buffer is empty,
|
|
23
|
+
// never while a previous prompt is still streaming). Suggestions route to the owner
|
|
24
|
+
// tab's transcript (inline mode) or the persona's reporting tab (external mode).
|
|
25
|
+
export class MonitorManager {
|
|
26
|
+
managers;
|
|
27
|
+
spawn;
|
|
28
|
+
flushMs;
|
|
29
|
+
monitors = new Map();
|
|
30
|
+
counter = 0;
|
|
31
|
+
constructor(managers, spawn = spawnMonitorSession, flushMs = MONITOR_FLUSH_MS) {
|
|
32
|
+
this.managers = managers;
|
|
33
|
+
this.spawn = spawn;
|
|
34
|
+
this.flushMs = flushMs;
|
|
35
|
+
}
|
|
36
|
+
// Start a monitor; returns an error message, or null on success. No targets = inline
|
|
37
|
+
// mode (watch the owner tab, report into its transcript).
|
|
38
|
+
start(owner, personaName, targets) {
|
|
39
|
+
const key = `${owner}:${personaName}`;
|
|
40
|
+
if (this.monitors.has(key))
|
|
41
|
+
return `Already monitoring with persona "${personaName}".`;
|
|
42
|
+
const inline = targets.length === 0;
|
|
43
|
+
const resolved = inline
|
|
44
|
+
? [{ kind: 'tab', label: owner }]
|
|
45
|
+
: resolveTargetAliases(this.managers.tab.tabs, targets);
|
|
46
|
+
const targetError = validateTargets(this.managers.tab.tabs, personaName, inline, resolved);
|
|
47
|
+
if (targetError)
|
|
48
|
+
return targetError;
|
|
49
|
+
let persona;
|
|
50
|
+
try {
|
|
51
|
+
persona = loadPersona(personaName);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return error instanceof Error ? error.message : String(error);
|
|
55
|
+
}
|
|
56
|
+
const reg = {
|
|
57
|
+
owner, inline, persona, targets: resolved, buffer: [], harnessSeen: new Map(), editorSeen: new Map(), pageSeen: new Map(), delimiter: generateSessionDelimiter(), inFlight: true, delivered: 0,
|
|
58
|
+
contextBytes: 0, contextText: [],
|
|
59
|
+
session: undefined, timer: undefined, subs: [],
|
|
60
|
+
};
|
|
61
|
+
this.openSession(reg);
|
|
62
|
+
this.subscribe(key, reg);
|
|
63
|
+
reg.buffer.push(...seedFeedEntries(this.managers, this.managers.tab.tabs, resolved, reg));
|
|
64
|
+
reg.timer = setInterval(() => this.flush(key), this.flushMs);
|
|
65
|
+
this.monitors.set(key, reg);
|
|
66
|
+
// External mode: open the reporting tab right away (empty feed) so starting the
|
|
67
|
+
// monitor is visible immediately, not only when the first suggestion lands.
|
|
68
|
+
if (!inline) {
|
|
69
|
+
openMonitorTab(this.managers, personaName, targetColor(this.managers.tab.tabs, resolved));
|
|
70
|
+
updateMonitorMeta(this.managers, personaName, formatTargets(resolved), reg.contextBytes);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// Spawn the monitor's dedicated session and prime it with the persona body + reply
|
|
75
|
+
// format; `inFlight` holds flushes off until priming settles.
|
|
76
|
+
openSession(reg) {
|
|
77
|
+
openMonitorSession(reg, this.managers, this.spawn);
|
|
78
|
+
}
|
|
79
|
+
// A prompt failed (typically the ACP subprocess died). Replace the session with a
|
|
80
|
+
// fresh, re-primed one so the monitor recovers instead of staying dead.
|
|
81
|
+
respawn(reg) {
|
|
82
|
+
respawnMonitorSession(reg, this.managers, this.spawn);
|
|
83
|
+
if (!reg.inline)
|
|
84
|
+
updateMonitorMeta(this.managers, reg.persona.name, formatTargets(reg.targets), reg.contextBytes);
|
|
85
|
+
}
|
|
86
|
+
subscribe(key, reg) {
|
|
87
|
+
reg.subs.push(messageBus.on('transcript', 'entry:appended', (event) => {
|
|
88
|
+
if (event.type !== 'entry:appended' || !matchesTargets(this.managers.tab.tabs, reg.targets, event.tabLabel))
|
|
89
|
+
return;
|
|
90
|
+
// Never feed a monitor its own (or a sibling monitor's) inline suggestions.
|
|
91
|
+
if (event.entry.output.startsWith(SUGGESTION_PREFIX))
|
|
92
|
+
return;
|
|
93
|
+
reg.buffer.push({ tabLabel: event.tabLabel, entry: event.entry });
|
|
94
|
+
}), messageBus.on('transcript', 'tab:removed', (event) => {
|
|
95
|
+
if (event.type !== 'tab:removed')
|
|
96
|
+
return;
|
|
97
|
+
if (event.tabLabel === reg.owner) {
|
|
98
|
+
this.handleOwnerClosed(reg.owner);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// The reporting tab itself was closed directly: tear the monitor down fully,
|
|
102
|
+
// regardless of remaining targets, so its session doesn't linger and the same
|
|
103
|
+
// owner/persona can be started again.
|
|
104
|
+
if (!reg.inline && event.tabLabel === reg.persona.name) {
|
|
105
|
+
this.stop(reg.owner, reg.persona.name);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Only tab targets drop out; group targets persist (the group may gain new tabs).
|
|
109
|
+
if (reg.targets.some((t) => t.kind === 'tab' && t.label === event.tabLabel)) {
|
|
110
|
+
this.stop(reg.owner, reg.persona.name, { kind: 'tab', label: event.tabLabel });
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
// The 30-second batch. No new transcripts → no ACP query at all; also skipped while a
|
|
115
|
+
// previous prompt (including the persona priming) is still streaming.
|
|
116
|
+
flush(key) {
|
|
117
|
+
const reg = this.monitors.get(key);
|
|
118
|
+
if (!reg || reg.inFlight)
|
|
119
|
+
return;
|
|
120
|
+
// Harness tabs never emit `entry:appended`, so top up from their rendered screen here — the
|
|
121
|
+
// live channel for harness targets. An idle harness yields nothing, keeping the "no new
|
|
122
|
+
// content → no ACP prompt" guarantee below intact.
|
|
123
|
+
reg.buffer.push(...flushFeedEntries(this.managers, reg.targets, reg));
|
|
124
|
+
if (reg.buffer.length === 0)
|
|
125
|
+
return;
|
|
126
|
+
const batch = reg.buffer;
|
|
127
|
+
reg.buffer = [];
|
|
128
|
+
const body = batch
|
|
129
|
+
.map(({ tabLabel, entry }) => frameEntry(tabLabel, entry, reg.delimiter))
|
|
130
|
+
.join('\n\n');
|
|
131
|
+
const prompt = `[Monitor update]\n${body}`;
|
|
132
|
+
recordContext(reg, prompt, 'input');
|
|
133
|
+
reg.inFlight = true;
|
|
134
|
+
let reply = '';
|
|
135
|
+
reg.session.prompt(prompt, {
|
|
136
|
+
onChunk: (text) => { reply += text; },
|
|
137
|
+
onEnd: () => {
|
|
138
|
+
reg.inFlight = false;
|
|
139
|
+
recordReply(reg, this.managers, reply);
|
|
140
|
+
const suggestion = parseSuggestion(reply);
|
|
141
|
+
if (suggestion)
|
|
142
|
+
this.deliver(reg, batch.at(-1).tabLabel, suggestion);
|
|
143
|
+
},
|
|
144
|
+
onError: (message) => {
|
|
145
|
+
this.managers.tab.append(reg.owner, { input: '', output: `monitor ${reg.persona.name}: ${message} — restarting monitor session` });
|
|
146
|
+
if (isRateLimitError(message))
|
|
147
|
+
notify(this.managers, 'rate-limited', reg.owner);
|
|
148
|
+
this.respawn(reg);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
deliver(reg, about, parsed) {
|
|
153
|
+
reg.delivered += 1;
|
|
154
|
+
const suggestion = buildSuggestion(parsed, reg.persona.name, about, `s-${++this.counter}`);
|
|
155
|
+
if (reg.inline) {
|
|
156
|
+
this.managers.tab.append(reg.owner, { input: '', output: formatInlineSuggestion(reg.persona.name, suggestion) });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
pushSuggestion(this.managers, reg.persona.name, targetColor(this.managers.tab.tabs, reg.targets), suggestion);
|
|
160
|
+
}
|
|
161
|
+
// Query a running monitor's ACP session directly; the reply lands in the owner tab's
|
|
162
|
+
// transcript. Shares the `inFlight` slot with flushes, so a question never interleaves
|
|
163
|
+
// with a monitor-update prompt. Returns an error message, or null when the question is
|
|
164
|
+
// on its way.
|
|
165
|
+
ask(owner, personaName, question) {
|
|
166
|
+
const reg = this.monitors.get(`${owner}:${personaName}`);
|
|
167
|
+
if (!reg)
|
|
168
|
+
return `No "${personaName}" monitor running from this tab.`;
|
|
169
|
+
if (reg.inFlight)
|
|
170
|
+
return `The ${personaName} monitor is busy; try again in a moment.`;
|
|
171
|
+
askMonitor(reg, owner, personaName, question, this.managers, () => this.respawn(reg));
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
// Thumbs up/down on a reporting-tab suggestion (see monitor-window `rateSuggestion`).
|
|
175
|
+
rate(id, up) {
|
|
176
|
+
rateSuggestion(this.monitors.values(), this.managers, id, up);
|
|
177
|
+
}
|
|
178
|
+
// Reset every monitor feeding `name`'s reporting tab to just its persona context.
|
|
179
|
+
resetContext(name) { for (const reg of this.monitors.values())
|
|
180
|
+
if (!reg.inline && reg.persona.name === name)
|
|
181
|
+
this.respawn(reg); }
|
|
182
|
+
// Open a point-in-time snapshot of `name`'s monitor context in an editor tab (see monitor-context).
|
|
183
|
+
snapshotContext(name) {
|
|
184
|
+
snapshotMonitorContext(this.monitors.values(), this.managers, name);
|
|
185
|
+
}
|
|
186
|
+
// Stop one persona's monitor (or drop a single target from it). Returns false when no
|
|
187
|
+
// such monitor exists.
|
|
188
|
+
stop(owner, personaName, target) {
|
|
189
|
+
return stopMonitor(this.monitors, this.managers, owner, personaName, target);
|
|
190
|
+
}
|
|
191
|
+
// The owning agent tab closed: stop its monitors, and close any reporting tab that no
|
|
192
|
+
// longer has a live monitor feeding it (another tab may still run the same persona).
|
|
193
|
+
handleOwnerClosed(owner) {
|
|
194
|
+
const personas = [...this.monitors.values()]
|
|
195
|
+
.filter((reg) => reg.owner === owner && !reg.inline)
|
|
196
|
+
.map((reg) => reg.persona.name);
|
|
197
|
+
this.stopAll(owner);
|
|
198
|
+
for (const name of personas)
|
|
199
|
+
closeIfUnfed(this.monitors, this.managers, name);
|
|
200
|
+
}
|
|
201
|
+
stopAll(owner) {
|
|
202
|
+
const mine = [...this.monitors.values()].filter((r) => r.owner === owner);
|
|
203
|
+
for (const reg of mine)
|
|
204
|
+
this.stop(reg.owner, reg.persona.name);
|
|
205
|
+
return mine.length;
|
|
206
|
+
}
|
|
207
|
+
// Lines for the `monitors` command.
|
|
208
|
+
list() {
|
|
209
|
+
return listMonitors(this.monitors.values());
|
|
210
|
+
}
|
|
211
|
+
// Connections-panel rows for a tab's monitors (e.g. `monitor:security (opencode/…)`).
|
|
212
|
+
connectionsFor(owner) {
|
|
213
|
+
return monitorConnections(this.monitors.values(), owner);
|
|
214
|
+
}
|
|
215
|
+
closeAll() {
|
|
216
|
+
// Snapshot first: `stop` mutates the map while we iterate.
|
|
217
|
+
const all = [...this.monitors.values()];
|
|
218
|
+
for (const reg of all)
|
|
219
|
+
this.stop(reg.owner, reg.persona.name);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resolveTargetTabs } from './targets.js';
|
|
2
|
+
import { diffFeedEntry } from './feed-diff.js';
|
|
3
|
+
// Turn page-view targets into monitor buffer entries. Page tabs have no `LogEntry` transcript, so a
|
|
4
|
+
// monitor watching one instead receives the text currently visible in that page's viewport, read
|
|
5
|
+
// synchronously from a per-tab cache (`tab.pageSnapshot`) the extension content script keeps fresh
|
|
6
|
+
// out of band via the `pageSync` RPC — never the DOM inline, since the flush this feeds is
|
|
7
|
+
// synchronous. The first feed to a given monitor for a given tab is the full current content; every
|
|
8
|
+
// one after that is a unified diff against what was last fed to *that* monitor, emitted only when
|
|
9
|
+
// the content actually changed. Every entry is byte-capped. Non-page targets are ignored here.
|
|
10
|
+
export function pageFeedEntries(managers, targets, pageSeen) {
|
|
11
|
+
const entries = [];
|
|
12
|
+
for (const tab of resolveTargetTabs(managers.tab.tabs, targets)) {
|
|
13
|
+
if (tab.view !== 'page' || !tab.pageSnapshot)
|
|
14
|
+
continue;
|
|
15
|
+
const current = tab.pageSnapshot.text;
|
|
16
|
+
const entry = diffFeedEntry(pageSeen, tab.label, current, tab.page?.domain ?? tab.label);
|
|
17
|
+
if (entry)
|
|
18
|
+
entries.push(entry);
|
|
19
|
+
}
|
|
20
|
+
return entries;
|
|
21
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// A target argument: `group:<n>` or a tab label.
|
|
2
|
+
function parseTarget(word) {
|
|
3
|
+
const group = /^group:(\d+)$/i.exec(word);
|
|
4
|
+
if (group)
|
|
5
|
+
return { kind: 'group', group: Number(group[1]) };
|
|
6
|
+
if (word.includes(':'))
|
|
7
|
+
return { error: `Bad target "${word}" (expected a tab label or group:<n>).` };
|
|
8
|
+
return { kind: 'tab', label: word };
|
|
9
|
+
}
|
|
10
|
+
// `monitor <persona> [target...]` — no targets means inline mode (watch the current tab).
|
|
11
|
+
// `monitor ask <persona> <question>` — query the running monitor's ACP directly.
|
|
12
|
+
export function parseMonitorCommand(input) {
|
|
13
|
+
const words = input.trim().split(/\s+/).slice(1);
|
|
14
|
+
if (words[0] === 'ask') {
|
|
15
|
+
const persona = words[1];
|
|
16
|
+
const question = words.slice(2).join(' ');
|
|
17
|
+
if (!persona || !question)
|
|
18
|
+
return { error: 'Usage: monitor ask <persona> <question>' };
|
|
19
|
+
return { ask: true, persona, question };
|
|
20
|
+
}
|
|
21
|
+
const persona = words[0];
|
|
22
|
+
if (!persona)
|
|
23
|
+
return { error: 'Usage: monitor <persona> [tab|group:<n> ...]' };
|
|
24
|
+
if (words[1] === 'ask')
|
|
25
|
+
return { error: `Did you mean: monitor ask ${persona} <question>?` };
|
|
26
|
+
const targets = [];
|
|
27
|
+
for (const word of words.slice(1)) {
|
|
28
|
+
const target = parseTarget(word);
|
|
29
|
+
if ('error' in target)
|
|
30
|
+
return target;
|
|
31
|
+
targets.push(target);
|
|
32
|
+
}
|
|
33
|
+
return { persona, targets };
|
|
34
|
+
}
|
|
35
|
+
// `unmonitor --all` | `unmonitor <persona> [target]`
|
|
36
|
+
export function parseUnmonitorCommand(input) {
|
|
37
|
+
const words = input.trim().split(/\s+/).slice(1);
|
|
38
|
+
if (words[0] === '--all')
|
|
39
|
+
return { all: true };
|
|
40
|
+
const persona = words[0];
|
|
41
|
+
if (!persona)
|
|
42
|
+
return { error: 'Usage: unmonitor <persona> [tab|group:<n>] | unmonitor --all' };
|
|
43
|
+
if (words.length === 1)
|
|
44
|
+
return { persona };
|
|
45
|
+
const target = parseTarget(words[1]);
|
|
46
|
+
if ('error' in target)
|
|
47
|
+
return target;
|
|
48
|
+
return { persona, target };
|
|
49
|
+
}
|
|
50
|
+
// Extract a deliverable report from the monitoring AI's reply. The reply may contain
|
|
51
|
+
// anything; only the marker lines count:
|
|
52
|
+
// [SUMMARY]: <text> — a recap of activity (harness/page), no command
|
|
53
|
+
// [SUGGESTION]: <text> — an actionable suggestion
|
|
54
|
+
// [COMMAND]: <optional command> — a command that accompanies a suggestion
|
|
55
|
+
// An actionable suggestion wins when both markers are present; a summary carries no
|
|
56
|
+
// command. No marker → nothing to deliver (the persona is told silence is fine).
|
|
57
|
+
// Extract the text following a `[MARKER]:` line, up to (but not including) the next
|
|
58
|
+
// bracket-marker line or the end of the reply — so a marker's own text may span multiple lines
|
|
59
|
+
// (e.g. a summary that continues onto bullet points).
|
|
60
|
+
function captureMarker(reply, marker) {
|
|
61
|
+
const re = new RegExp(String.raw `(?:^|\n)\[${marker}]:\s*([\s\S]*?)(?=\n\[[A-Z]+]:|$)`);
|
|
62
|
+
return re.exec(reply)?.[1]?.trim();
|
|
63
|
+
}
|
|
64
|
+
export function parseSuggestion(reply) {
|
|
65
|
+
const suggestion = captureMarker(reply, 'SUGGESTION');
|
|
66
|
+
if (suggestion) {
|
|
67
|
+
const command = captureMarker(reply, 'COMMAND');
|
|
68
|
+
return command ? { text: suggestion, command } : { text: suggestion };
|
|
69
|
+
}
|
|
70
|
+
const summary = captureMarker(reply, 'SUMMARY');
|
|
71
|
+
return summary ? { text: summary } : null;
|
|
72
|
+
}
|
|
73
|
+
// The output-format instructions appended to every persona's startup prompt.
|
|
74
|
+
export const SUGGESTION_FORMAT = [
|
|
75
|
+
'Reply using exactly one of these formats:',
|
|
76
|
+
'To recap what a harness or web page is doing (a summary, not a suggestion):',
|
|
77
|
+
'[SUMMARY]: <one or two short sentences>',
|
|
78
|
+
'To offer an actionable suggestion:',
|
|
79
|
+
'[SUGGESTION]: <one short sentence>',
|
|
80
|
+
'[COMMAND]: <a single command the user could run, only if one clearly applies>',
|
|
81
|
+
'Only when you have genuinely nothing to report, reply with the single word: OK',
|
|
82
|
+
].join('\n');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { formatTargets } from './targets.js';
|
|
2
|
+
import { updateMonitorMeta } from './window.js';
|
|
3
|
+
import { recordContext } from './context.js';
|
|
4
|
+
// Account for a completed prompt's reply and refresh the reporting tab's metadata — shared by
|
|
5
|
+
// flush's and ask's `onEnd` handlers, both of which do this identically once an ACP reply finishes
|
|
6
|
+
// streaming.
|
|
7
|
+
export function recordReply(reg, managers, reply) {
|
|
8
|
+
recordContext(reg, reply, 'response');
|
|
9
|
+
if (!reg.inline)
|
|
10
|
+
updateMonitorMeta(managers, reg.persona.name, formatTargets(reg.targets), reg.contextBytes);
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { messageBus } from '../bus.js';
|
|
2
|
+
import { spawnMonitorSession } from './acp.js';
|
|
3
|
+
import { SUGGESTION_FORMAT } from './parsing.js';
|
|
4
|
+
import { TRUST_FRAMING_INSTRUCTIONS } from './framing.js';
|
|
5
|
+
import { formatConnection, personaSummary } from './info.js';
|
|
6
|
+
import { recordContext } from './context.js';
|
|
7
|
+
export function openMonitorSession(reg, managers, spawn = spawnMonitorSession) {
|
|
8
|
+
reg.inFlight = true;
|
|
9
|
+
reg.session = spawn(reg.persona, managers.tab.cwdOf(reg.owner) ?? process.cwd(), {
|
|
10
|
+
onError: (message) => managers.tab.append(reg.owner, { input: '', output: `monitor ${reg.persona.name}: ${message}` }),
|
|
11
|
+
onConnect: (info) => {
|
|
12
|
+
reg.info = info;
|
|
13
|
+
const connection = formatConnection(info);
|
|
14
|
+
const summary = personaSummary(reg.persona);
|
|
15
|
+
managers.tab.append(reg.owner, {
|
|
16
|
+
input: '',
|
|
17
|
+
output: `monitor ${reg.persona.name}: connected${connection ? ` (${connection})` : ''} — ${summary}`,
|
|
18
|
+
});
|
|
19
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const primingText = `${reg.persona.body}\n\n${SUGGESTION_FORMAT}\n\n${TRUST_FRAMING_INSTRUCTIONS(reg.delimiter)}`;
|
|
23
|
+
recordContext(reg, primingText, 'input');
|
|
24
|
+
reg.session.prompt(primingText, {
|
|
25
|
+
onChunk: () => { },
|
|
26
|
+
onEnd: () => { reg.inFlight = false; },
|
|
27
|
+
onError: () => { reg.inFlight = false; },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function respawnMonitorSession(reg, managers, spawn = spawnMonitorSession) {
|
|
31
|
+
reg.session.kill();
|
|
32
|
+
// A fresh session starts a fresh context.
|
|
33
|
+
reg.contextBytes = 0;
|
|
34
|
+
reg.contextText = [];
|
|
35
|
+
openMonitorSession(reg, managers, spawn);
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { closeMonitorTab, updateMonitorMeta } from './window.js';
|
|
2
|
+
import { formatTargets, resolveTargetAliases } from './targets.js';
|
|
3
|
+
// Stop one persona's monitor (or drop a single target from it). Returns false when no such
|
|
4
|
+
// monitor exists. Split out of MonitorManager to keep that file under the size limit.
|
|
5
|
+
export function stopMonitor(monitors, managers, owner, personaName, target) {
|
|
6
|
+
const key = `${owner}:${personaName}`;
|
|
7
|
+
const reg = monitors.get(key);
|
|
8
|
+
if (!reg)
|
|
9
|
+
return false;
|
|
10
|
+
const resolvedTarget = target ? resolveTargetAliases(managers.tab.tabs, [target])[0] : undefined;
|
|
11
|
+
if (resolvedTarget && !reg.inline) {
|
|
12
|
+
reg.targets = reg.targets.filter((t) => JSON.stringify(t) !== JSON.stringify(resolvedTarget));
|
|
13
|
+
if (reg.targets.length > 0) {
|
|
14
|
+
updateMonitorMeta(managers, personaName, formatTargets(reg.targets), reg.contextBytes);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const sub of reg.subs)
|
|
19
|
+
sub.unsubscribe();
|
|
20
|
+
clearInterval(reg.timer);
|
|
21
|
+
reg.session.kill();
|
|
22
|
+
monitors.delete(key);
|
|
23
|
+
if (!reg.inline)
|
|
24
|
+
closeIfUnfed(monitors, managers, personaName);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
// Close a persona's reporting tab if no live monitor still feeds it (another owner may run
|
|
28
|
+
// the same persona and keep it open).
|
|
29
|
+
export function closeIfUnfed(monitors, managers, personaName) {
|
|
30
|
+
const stillFed = [...monitors.values()].some((r) => !r.inline && r.persona.name === personaName);
|
|
31
|
+
if (!stillFed)
|
|
32
|
+
closeMonitorTab(managers, personaName);
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Marks inline suggestion entries so monitors never feed on their own output.
|
|
2
|
+
export const SUGGESTION_PREFIX = '💡';
|
|
3
|
+
// Builds the suggestion record delivered to either the owner tab's transcript (inline mode) or
|
|
4
|
+
// the persona's reporting tab (external mode).
|
|
5
|
+
export function buildSuggestion(parsed, personaName, about, id) {
|
|
6
|
+
return { ...parsed, id, timestamp: Date.now(), persona: personaName, about };
|
|
7
|
+
}
|
|
8
|
+
// Formats an inline-mode suggestion as it's appended to the owner tab's transcript.
|
|
9
|
+
export function formatInlineSuggestion(personaName, suggestion) {
|
|
10
|
+
const command = suggestion.command ? `\n${suggestion.command}` : '';
|
|
11
|
+
return `${SUGGESTION_PREFIX} ${personaName}: ${suggestion.text}${command}`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Target helpers for the monitor manager: validation, live matching, and the color a
|
|
2
|
+
// reporting tab inherits. All operate on the current tab list — group targets are
|
|
3
|
+
// resolved at event time, so group membership stays dynamic.
|
|
4
|
+
// The current tabs a monitor's targets resolve to: a tab target's tab, or every member of a group
|
|
5
|
+
// target. Shared by the initial-seed loop and the harness feed so both resolve targets identically.
|
|
6
|
+
export function resolveTargetTabs(tabs, targets) {
|
|
7
|
+
return tabs.filter((t) => targets.some((target) => target.kind === 'tab' ? t.label === target.label : t.group === target.group));
|
|
8
|
+
}
|
|
9
|
+
// The buffer entries to seed a monitor with at start: every existing `LogEntry` of each target tab,
|
|
10
|
+
// tagged with that tab's label.
|
|
11
|
+
export function seedEntries(tabs, targets) {
|
|
12
|
+
const entries = [];
|
|
13
|
+
for (const tab of resolveTargetTabs(tabs, targets)) {
|
|
14
|
+
for (const entry of tab.log)
|
|
15
|
+
entries.push({ tabLabel: tab.label, entry });
|
|
16
|
+
}
|
|
17
|
+
return entries;
|
|
18
|
+
}
|
|
19
|
+
// Resolve a tab-kind target's typed label against a tab's canonical label or display alias
|
|
20
|
+
// (see `rename`), case-insensitively — mirrors `resolveTarget` in commands/resolve-target.ts.
|
|
21
|
+
// A target that matches no tab passes through unchanged, so `validateTargets` still reports
|
|
22
|
+
// it as missing. Group targets pass through unchanged.
|
|
23
|
+
export function resolveTargetAliases(tabs, targets) {
|
|
24
|
+
return targets.map((target) => {
|
|
25
|
+
if (target.kind !== 'tab')
|
|
26
|
+
return target;
|
|
27
|
+
const key = target.label.toLowerCase();
|
|
28
|
+
const match = tabs.find((t) => t.label.toLowerCase() === key || t.title?.toLowerCase() === key);
|
|
29
|
+
return match ? { kind: 'tab', label: match.label } : target;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function validateTargets(tabs, personaName, inline, targets) {
|
|
33
|
+
for (const target of targets) {
|
|
34
|
+
if (target.kind === 'tab' && tabs.every((t) => t.label !== target.label || t.view === 'monitor')) {
|
|
35
|
+
return `No tab named "${target.label}".`;
|
|
36
|
+
}
|
|
37
|
+
if (target.kind === 'group' && tabs.every((t) => t.group !== target.group)) {
|
|
38
|
+
return `No group ${target.group}.`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// The reporting tab reuses the persona name as its label; an action tab holding that
|
|
42
|
+
// label would collide with by-label lookups (e.g. runSuggestion's dispatch).
|
|
43
|
+
if (!inline && tabs.some((t) => t.label === personaName && t.view !== 'monitor')) {
|
|
44
|
+
return `A tab named "${personaName}" already exists; rename it or pick another persona.`;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// A human-readable rendering of a monitor's targets, e.g. `agent2, group:3`.
|
|
49
|
+
export function formatTargets(targets) {
|
|
50
|
+
return targets.map((t) => (t.kind === 'tab' ? t.label : `group:${t.group}`)).join(', ');
|
|
51
|
+
}
|
|
52
|
+
export function matchesTargets(tabs, targets, tabLabel) {
|
|
53
|
+
const tab = tabs.find((t) => t.label === tabLabel);
|
|
54
|
+
if (!tab || tab.view === 'monitor')
|
|
55
|
+
return false;
|
|
56
|
+
return targets.some((t) => (t.kind === 'tab' ? t.label === tabLabel : t.group === tab.group));
|
|
57
|
+
}
|
|
58
|
+
// Reporting tabs carry the monitored tab's color: the first tab target's dot color, or
|
|
59
|
+
// the first member of the first group target.
|
|
60
|
+
export function targetColor(tabs, targets) {
|
|
61
|
+
for (const target of targets) {
|
|
62
|
+
const tab = target.kind === 'tab'
|
|
63
|
+
? tabs.find((t) => t.label === target.label)
|
|
64
|
+
: tabs.find((t) => t.group === target.group);
|
|
65
|
+
if (tab)
|
|
66
|
+
return target.kind === 'group' ? tab.groupColor : tab.dotColor;
|
|
67
|
+
}
|
|
68
|
+
return '#5b9cff';
|
|
69
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { makeTab } from '../tab/index.js';
|
|
2
|
+
import { messageBus } from '../bus.js';
|
|
3
|
+
// Monitor reporting tabs: each external-mode monitor gets its own view-only tab
|
|
4
|
+
// (`view: 'monitor'`) named after its persona (e.g. `security`, `quality`) and colored
|
|
5
|
+
// after the tab it monitors — the reporting strip and body left-border carry that color.
|
|
6
|
+
// Reporting tabs render in the reporting section below the command bar, never in the
|
|
7
|
+
// action strip, and accept no commands; their only interactions are the per-suggestion
|
|
8
|
+
// Run/Dismiss buttons (RPCs handled here).
|
|
9
|
+
const makeMonitorTab = (name, dotColor, number) => ({
|
|
10
|
+
// Group 0: reporting tabs sit outside the action-tab group system.
|
|
11
|
+
...makeTab(name, dotColor, number, [], [], undefined, 0, dotColor),
|
|
12
|
+
view: 'monitor',
|
|
13
|
+
title: name,
|
|
14
|
+
// The reporting tab's label is always its persona name (see `MonitorManager.start`), so
|
|
15
|
+
// `persona` never changes after creation; `targets`/`contextBytes` are filled in afterward
|
|
16
|
+
// via `updateMonitorMeta` once the owning monitor registration exists.
|
|
17
|
+
monitor: { suggestions: [], persona: name, targets: '', contextBytes: 0 },
|
|
18
|
+
});
|
|
19
|
+
// A unique label for a new monitor's reporting tab: the persona name, suffixed
|
|
20
|
+
// (`assistant-2`, …) when that label is already taken by any tab. Each monitor instance
|
|
21
|
+
// gets its own window, so the same persona can watch different targets side by side.
|
|
22
|
+
export function allocateMonitorLabel(managers, persona) {
|
|
23
|
+
const used = new Set(managers.tab.tabs.map((t) => t.label));
|
|
24
|
+
if (!used.has(persona))
|
|
25
|
+
return persona;
|
|
26
|
+
let n = 2;
|
|
27
|
+
while (used.has(`${persona}-${n}`))
|
|
28
|
+
n++;
|
|
29
|
+
return `${persona}-${n}`;
|
|
30
|
+
}
|
|
31
|
+
// All monitor reporting tabs currently open.
|
|
32
|
+
export function monitorTabs(managers) {
|
|
33
|
+
return managers.tab.tabs.filter((t) => t.view === 'monitor');
|
|
34
|
+
}
|
|
35
|
+
// Open the named monitor's reporting tab or reuse the existing one. Reporting tabs are
|
|
36
|
+
// appended at the end of the tab list so action-tab indices (including `activeTab`)
|
|
37
|
+
// never shift, and the active tab is left untouched.
|
|
38
|
+
export function openMonitorTab(managers, name, dotColor) {
|
|
39
|
+
const existing = monitorTabs(managers).find((t) => t.label === name);
|
|
40
|
+
if (existing)
|
|
41
|
+
return existing;
|
|
42
|
+
const tabs = managers.tab.tabs;
|
|
43
|
+
const tab = makeMonitorTab(name, dotColor, tabs.length + 1);
|
|
44
|
+
managers.tab.tabs = [...tabs, tab];
|
|
45
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
46
|
+
return tab;
|
|
47
|
+
}
|
|
48
|
+
// Append a suggestion to the named monitor's feed (opening its tab if needed).
|
|
49
|
+
export function pushSuggestion(managers, name, dotColor, suggestion) {
|
|
50
|
+
const tab = openMonitorTab(managers, name, dotColor);
|
|
51
|
+
tab.monitor.suggestions.push(suggestion);
|
|
52
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
53
|
+
}
|
|
54
|
+
// Update a persona's reporting tab with its monitor's current targets and running
|
|
55
|
+
// context-byte total. A no-op if the tab or its monitor payload is gone.
|
|
56
|
+
export function updateMonitorMeta(managers, name, targets, contextBytes) {
|
|
57
|
+
const tab = monitorTabs(managers).find((t) => t.label === name);
|
|
58
|
+
if (!tab?.monitor)
|
|
59
|
+
return;
|
|
60
|
+
tab.monitor.targets = targets;
|
|
61
|
+
tab.monitor.contextBytes = contextBytes;
|
|
62
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
63
|
+
}
|
|
64
|
+
// Close a persona's reporting tab (used when the last monitor feeding it goes away
|
|
65
|
+
// with its owning agent tab).
|
|
66
|
+
export function closeMonitorTab(managers, name) {
|
|
67
|
+
const index = managers.tab.tabs.findIndex((t) => t.view === 'monitor' && t.label === name);
|
|
68
|
+
if (index !== -1)
|
|
69
|
+
managers.tab.closeTab(index);
|
|
70
|
+
}
|
|
71
|
+
// Find a suggestion anywhere in the monitor feeds by id.
|
|
72
|
+
export function findSuggestion(managers, id) {
|
|
73
|
+
return monitorTabs(managers)
|
|
74
|
+
.flatMap((t) => t.monitor.suggestions)
|
|
75
|
+
.find((s) => s.id === id);
|
|
76
|
+
}
|
|
77
|
+
// Remove a suggestion from whichever feed holds it (thumbs-down).
|
|
78
|
+
export function removeSuggestion(managers, id) {
|
|
79
|
+
for (const tab of monitorTabs(managers)) {
|
|
80
|
+
const before = tab.monitor.suggestions.length;
|
|
81
|
+
tab.monitor.suggestions = tab.monitor.suggestions.filter((s) => s.id !== id);
|
|
82
|
+
if (tab.monitor.suggestions.length !== before)
|
|
83
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Run a suggestion's command in the tab the suggestion is about (a clicked command
|
|
87
|
+
// line). The suggestion stays in the feed — the history of what was suggested (and run)
|
|
88
|
+
// is part of the record. Monitor tabs themselves never execute commands.
|
|
89
|
+
export function runSuggestion(managers, id) {
|
|
90
|
+
const suggestion = findSuggestion(managers, id);
|
|
91
|
+
if (!suggestion?.command)
|
|
92
|
+
return;
|
|
93
|
+
managers.command.dispatchTo(suggestion.about, suggestion.command);
|
|
94
|
+
}
|
|
95
|
+
// Thumbs up/down on a reporting-tab suggestion. The rating is fed back to the monitor through its
|
|
96
|
+
// normal batched prompt channel (queued on the owning monitor's buffer, no extra ACP round-trip),
|
|
97
|
+
// so the AI learns what the user found useful. Rating a suggestion means the user is done with it,
|
|
98
|
+
// so either direction removes it from the feed.
|
|
99
|
+
export function rateSuggestion(monitors, managers, id, up) {
|
|
100
|
+
const suggestion = findSuggestion(managers, id);
|
|
101
|
+
if (!suggestion)
|
|
102
|
+
return;
|
|
103
|
+
const reg = [...monitors].find((r) => !r.inline && r.persona.name === suggestion.persona);
|
|
104
|
+
reg?.buffer.push({
|
|
105
|
+
tabLabel: suggestion.about,
|
|
106
|
+
entry: { input: '', output: `[user feedback] The user rated your suggestion "${suggestion.text}" as ${up ? 'helpful (thumbs up)' : 'not helpful (thumbs down)'}.` },
|
|
107
|
+
});
|
|
108
|
+
removeSuggestion(managers, id);
|
|
109
|
+
}
|