synabun 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — Terminal Linking (Relay/Mediator)
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Floating Link Panel that mediates turn-based conversations
|
|
6
|
+
// between AI agents (Claude Code, Codex, Gemini, etc.) via linked
|
|
7
|
+
// terminal sessions. Users can inject messages, control auto-relay,
|
|
8
|
+
// and watch agents collaborate in real-time.
|
|
9
|
+
|
|
10
|
+
import { state, emit, on } from './state.js';
|
|
11
|
+
import { KEYS } from './constants.js';
|
|
12
|
+
import { storage } from './storage.js';
|
|
13
|
+
import {
|
|
14
|
+
fetchTerminalSessions, fetchTerminalLinks, fetchTerminalLink,
|
|
15
|
+
createTerminalLink, deleteTerminalLink, updateTerminalLink,
|
|
16
|
+
sendLinkMessage, pauseLink, resumeLink, nudgeLink,
|
|
17
|
+
} from './api.js';
|
|
18
|
+
|
|
19
|
+
// ── SVG Icons ──
|
|
20
|
+
const SVG_LINK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
|
|
21
|
+
const SVG_UNLINK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18.84 12.25l1.72-1.71a5 5 0 0 0-7.07-7.07l-3 3"/><path d="M5.16 11.75l-1.72 1.71a5 5 0 0 0 7.07 7.07l3-3"/><line x1="2" y1="2" x2="22" y2="22"/></svg>';
|
|
22
|
+
const SVG_SEND = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
|
|
23
|
+
const SVG_PAUSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';
|
|
24
|
+
const SVG_PLAY = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>';
|
|
25
|
+
const SVG_SKIP = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/></svg>';
|
|
26
|
+
const SVG_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
27
|
+
const SVG_MIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>';
|
|
28
|
+
const SVG_COPY = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
29
|
+
|
|
30
|
+
// Profile metadata (mirror of ui-terminal.js PROFILES)
|
|
31
|
+
const PROFILE_META = {
|
|
32
|
+
'claude-code': { label: 'Claude Code', color: '#D4A27F' },
|
|
33
|
+
'codex': { label: 'Codex CLI', color: '#74c7a5' },
|
|
34
|
+
'gemini': { label: 'Gemini CLI', color: '#669DF6' },
|
|
35
|
+
'shell': { label: 'Shell', color: '#aaaaaa' },
|
|
36
|
+
'browser': { label: 'Browser', color: '#4fc3f7' },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Module State ──
|
|
40
|
+
let _panel = null; // floating panel DOM element
|
|
41
|
+
let _activeLink = null; // current link ID
|
|
42
|
+
let _history = []; // local copy of conversation history
|
|
43
|
+
let _minimized = false;
|
|
44
|
+
let _visible = false;
|
|
45
|
+
|
|
46
|
+
// Drag state
|
|
47
|
+
let _dragState = null;
|
|
48
|
+
// Resize state
|
|
49
|
+
let _resizeState = null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Open the Link Panel. If no link exists, shows setup view.
|
|
53
|
+
*/
|
|
54
|
+
export async function openLinkPanel() {
|
|
55
|
+
console.log('[link-panel] openLinkPanel called, _panel=', !!_panel, '_visible=', _visible);
|
|
56
|
+
if (_panel) {
|
|
57
|
+
_panel.style.display = '';
|
|
58
|
+
_visible = true;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
_buildPanel();
|
|
62
|
+
_visible = true;
|
|
63
|
+
|
|
64
|
+
// Check for existing links
|
|
65
|
+
try {
|
|
66
|
+
const data = await fetchTerminalLinks();
|
|
67
|
+
const links = data?.links || [];
|
|
68
|
+
console.log('[link-panel] existing links:', links.length);
|
|
69
|
+
if (links.length > 0) {
|
|
70
|
+
_activeLink = links[0].id;
|
|
71
|
+
await _loadLink(_activeLink);
|
|
72
|
+
} else {
|
|
73
|
+
await _showSetupView();
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('[link-panel] Error checking links:', err);
|
|
77
|
+
await _showSetupView();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Toggle the Link Panel visibility.
|
|
83
|
+
*/
|
|
84
|
+
export function toggleLinkPanel() {
|
|
85
|
+
if (_visible) {
|
|
86
|
+
closeLinkPanel();
|
|
87
|
+
} else {
|
|
88
|
+
openLinkPanel();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Close (hide) the Link Panel without destroying the link.
|
|
94
|
+
*/
|
|
95
|
+
export function closeLinkPanel() {
|
|
96
|
+
if (_panel) {
|
|
97
|
+
_panel.style.display = 'none';
|
|
98
|
+
_visible = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Panel Construction ──
|
|
103
|
+
|
|
104
|
+
function _buildPanel() {
|
|
105
|
+
_panel = document.createElement('div');
|
|
106
|
+
_panel.className = 'link-panel';
|
|
107
|
+
|
|
108
|
+
// Restore saved position/size
|
|
109
|
+
let savedPos = null, savedSize = null;
|
|
110
|
+
try { savedPos = JSON.parse(storage.getItem(KEYS.LINK_PANEL_POS)); } catch {}
|
|
111
|
+
try { savedSize = JSON.parse(storage.getItem(KEYS.LINK_PANEL_SIZE)); } catch {}
|
|
112
|
+
|
|
113
|
+
_panel.style.left = savedPos?.x ? `${savedPos.x}px` : '50%';
|
|
114
|
+
_panel.style.top = savedPos?.y ? `${Math.max(48, savedPos.y)}px` : '50%';
|
|
115
|
+
if (!savedPos) _panel.style.transform = 'translate(-50%, -50%)';
|
|
116
|
+
_panel.style.width = savedSize?.w ? `${savedSize.w}px` : '480px';
|
|
117
|
+
_panel.style.height = savedSize?.h ? `${savedSize.h}px` : '560px';
|
|
118
|
+
|
|
119
|
+
_panel.innerHTML = `
|
|
120
|
+
<div class="link-panel-header">
|
|
121
|
+
<span class="link-panel-icon">${SVG_LINK}</span>
|
|
122
|
+
<span class="link-panel-title">Terminal Link</span>
|
|
123
|
+
<div class="link-panel-actions">
|
|
124
|
+
<button class="link-panel-btn" data-action="minimize" title="Minimize">${SVG_MIN}</button>
|
|
125
|
+
<button class="link-panel-btn" data-action="close" title="Close">${SVG_CLOSE}</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="link-panel-agents"></div>
|
|
129
|
+
<div class="link-panel-controls"></div>
|
|
130
|
+
<div class="link-panel-body">
|
|
131
|
+
<div class="link-panel-messages"></div>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="link-panel-input-area">
|
|
134
|
+
<div class="link-panel-target"></div>
|
|
135
|
+
<div class="link-panel-input-row">
|
|
136
|
+
<textarea class="link-panel-input" placeholder="Type a message to send..." rows="1"></textarea>
|
|
137
|
+
<button class="link-panel-send" title="Send">${SVG_SEND}</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
document.body.appendChild(_panel);
|
|
143
|
+
|
|
144
|
+
// ── Wire header actions ──
|
|
145
|
+
_panel.querySelector('[data-action="close"]').onclick = () => closeLinkPanel();
|
|
146
|
+
_panel.querySelector('[data-action="minimize"]').onclick = () => _minimize();
|
|
147
|
+
|
|
148
|
+
// ── Wire drag ──
|
|
149
|
+
const header = _panel.querySelector('.link-panel-header');
|
|
150
|
+
header.addEventListener('mousedown', _onDragStart);
|
|
151
|
+
|
|
152
|
+
// ── Wire resize (edge-based) ──
|
|
153
|
+
_panel.addEventListener('mousemove', _onResizeHover);
|
|
154
|
+
_panel.addEventListener('mousedown', _onResizeStart);
|
|
155
|
+
|
|
156
|
+
// ── Wire input ──
|
|
157
|
+
const textarea = _panel.querySelector('.link-panel-input');
|
|
158
|
+
const sendBtn = _panel.querySelector('.link-panel-send');
|
|
159
|
+
sendBtn.onclick = () => _handleSend();
|
|
160
|
+
textarea.addEventListener('keydown', (e) => {
|
|
161
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
_handleSend();
|
|
164
|
+
}
|
|
165
|
+
// Auto-resize textarea
|
|
166
|
+
requestAnimationFrame(() => {
|
|
167
|
+
textarea.style.height = 'auto';
|
|
168
|
+
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Setup View (no active link — pick terminals to link) ──
|
|
174
|
+
|
|
175
|
+
async function _showSetupView() {
|
|
176
|
+
console.log('[link-panel] _showSetupView called');
|
|
177
|
+
const agentsEl = _panel.querySelector('.link-panel-agents');
|
|
178
|
+
const controlsEl = _panel.querySelector('.link-panel-controls');
|
|
179
|
+
const bodyEl = _panel.querySelector('.link-panel-body');
|
|
180
|
+
const inputArea = _panel.querySelector('.link-panel-input-area');
|
|
181
|
+
|
|
182
|
+
controlsEl.innerHTML = '';
|
|
183
|
+
inputArea.style.display = 'none';
|
|
184
|
+
|
|
185
|
+
// Fetch active terminal sessions
|
|
186
|
+
let sessions = [];
|
|
187
|
+
try {
|
|
188
|
+
const data = await fetchTerminalSessions();
|
|
189
|
+
sessions = data.sessions || [];
|
|
190
|
+
console.log('[link-panel] found', sessions.length, 'terminal sessions');
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error('[link-panel] Error fetching sessions:', err);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (sessions.length < 2) {
|
|
196
|
+
agentsEl.innerHTML = '';
|
|
197
|
+
bodyEl.innerHTML = `
|
|
198
|
+
<div class="link-panel-empty">
|
|
199
|
+
<div class="link-panel-empty-icon">${SVG_UNLINK}</div>
|
|
200
|
+
<p>Open at least 2 terminal sessions to create a link.</p>
|
|
201
|
+
<p style="opacity:0.5;font-size:12px;">Use the terminal panel to launch Claude Code, Codex, or Gemini.</p>
|
|
202
|
+
</div>
|
|
203
|
+
`;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Show session picker
|
|
208
|
+
agentsEl.innerHTML = '';
|
|
209
|
+
bodyEl.innerHTML = `
|
|
210
|
+
<div class="link-setup">
|
|
211
|
+
<div class="link-setup-title">Select terminals to link</div>
|
|
212
|
+
<div class="link-setup-sessions">${sessions.map(s => {
|
|
213
|
+
const meta = PROFILE_META[s.profile] || { label: s.profile, color: '#888' };
|
|
214
|
+
return `
|
|
215
|
+
<label class="link-setup-session" data-id="${s.id}">
|
|
216
|
+
<input type="checkbox" value="${s.id}" />
|
|
217
|
+
<span class="link-setup-dot" style="background:${meta.color}"></span>
|
|
218
|
+
<span class="link-setup-label">${meta.label}</span>
|
|
219
|
+
<span class="link-setup-cwd">${s.cwd ? s.cwd.split(/[/\\]/).pop() : ''}</span>
|
|
220
|
+
</label>
|
|
221
|
+
`;
|
|
222
|
+
}).join('')}</div>
|
|
223
|
+
<div class="link-setup-options">
|
|
224
|
+
<label class="link-setup-option">
|
|
225
|
+
<input type="checkbox" id="link-auto-continue" checked />
|
|
226
|
+
<span>Auto-continue (agents relay automatically)</span>
|
|
227
|
+
</label>
|
|
228
|
+
<div class="link-setup-mode">
|
|
229
|
+
<span class="link-setup-mode-label">Mode:</span>
|
|
230
|
+
<div class="link-setup-mode-toggle">
|
|
231
|
+
<button class="link-mode-btn selected" data-mode="sidecar" title="Headless relay — clean text, terminals untouched">Sidecar</button>
|
|
232
|
+
<button class="link-mode-btn" data-mode="live" title="Live — messages injected into actual TUI terminals">Live</button>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
<button class="link-setup-create">Create Link</button>
|
|
237
|
+
</div>
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
// Wire mode toggle buttons
|
|
241
|
+
bodyEl.querySelectorAll('.link-mode-btn').forEach(btn => {
|
|
242
|
+
btn.onclick = () => {
|
|
243
|
+
bodyEl.querySelectorAll('.link-mode-btn').forEach(b => b.classList.remove('selected'));
|
|
244
|
+
btn.classList.add('selected');
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const createBtn = bodyEl.querySelector('.link-setup-create');
|
|
249
|
+
createBtn.onclick = async () => {
|
|
250
|
+
const checked = [...bodyEl.querySelectorAll('.link-setup-sessions input:checked')].map(el => el.value);
|
|
251
|
+
if (checked.length < 2) return;
|
|
252
|
+
const autoContinue = bodyEl.querySelector('#link-auto-continue').checked;
|
|
253
|
+
const mode = bodyEl.querySelector('.link-mode-btn.selected')?.dataset.mode || 'sidecar';
|
|
254
|
+
|
|
255
|
+
createBtn.disabled = true;
|
|
256
|
+
createBtn.textContent = 'Creating...';
|
|
257
|
+
try {
|
|
258
|
+
const { linkId } = await createTerminalLink(checked, { autoContinue, mode });
|
|
259
|
+
_activeLink = linkId;
|
|
260
|
+
await _loadLink(linkId);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
createBtn.disabled = false;
|
|
263
|
+
createBtn.textContent = 'Create Link';
|
|
264
|
+
console.error('[link] Create failed:', err);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Load & Render Active Link ──
|
|
270
|
+
|
|
271
|
+
async function _loadLink(linkId) {
|
|
272
|
+
try {
|
|
273
|
+
const link = await fetchTerminalLink(linkId);
|
|
274
|
+
_history = link.history || [];
|
|
275
|
+
|
|
276
|
+
// Update shared state for tab badges
|
|
277
|
+
state.linkedSessionIds.clear();
|
|
278
|
+
link.sessions.forEach(s => state.linkedSessionIds.add(s.id));
|
|
279
|
+
emit('terminal:tabs-changed');
|
|
280
|
+
|
|
281
|
+
// Restore conversation DOM if the setup view replaced it
|
|
282
|
+
const body = _panel.querySelector('.link-panel-body');
|
|
283
|
+
if (!body.querySelector('.link-panel-messages')) {
|
|
284
|
+
body.innerHTML = '<div class="link-panel-messages"></div>';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_renderAgents(link);
|
|
288
|
+
_renderControls(link);
|
|
289
|
+
_renderMessages();
|
|
290
|
+
_panel.querySelector('.link-panel-input-area').style.display = '';
|
|
291
|
+
_renderTargetSelector(link);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error('[link] Load failed:', err);
|
|
294
|
+
_showSetupView();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function _renderAgents(link) {
|
|
299
|
+
const el = _panel.querySelector('.link-panel-agents');
|
|
300
|
+
el.innerHTML = link.sessions.map((s, i) => {
|
|
301
|
+
const meta = PROFILE_META[s.profile] || { label: s.profile, color: '#888' };
|
|
302
|
+
const active = i === link.activeAgent && link.status === 'running';
|
|
303
|
+
return `
|
|
304
|
+
<div class="link-agent ${active ? 'active' : ''}" data-idx="${i}">
|
|
305
|
+
<span class="link-agent-dot" style="background:${meta.color}"></span>
|
|
306
|
+
<span class="link-agent-name">${meta.label}</span>
|
|
307
|
+
${active ? '<span class="link-agent-typing">typing...</span>' : ''}
|
|
308
|
+
</div>
|
|
309
|
+
`;
|
|
310
|
+
}).join('<span class="link-agent-arrow">⇄</span>')
|
|
311
|
+
+ `<span class="link-mode-badge ${link.mode || 'sidecar'}">${(link.mode || 'sidecar').toUpperCase()}</span>`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function _renderControls(link) {
|
|
315
|
+
const el = _panel.querySelector('.link-panel-controls');
|
|
316
|
+
const isRunning = link.status === 'running';
|
|
317
|
+
const autoContinue = link.config?.autoContinue;
|
|
318
|
+
|
|
319
|
+
el.innerHTML = `
|
|
320
|
+
<div class="link-controls">
|
|
321
|
+
<button class="link-ctrl-btn ${autoContinue ? 'active' : ''}" data-action="toggle-auto" title="${autoContinue ? 'Auto-relay ON' : 'Auto-relay OFF'}">
|
|
322
|
+
<span class="link-ctrl-dot ${autoContinue ? 'on' : 'off'}"></span>
|
|
323
|
+
Auto
|
|
324
|
+
</button>
|
|
325
|
+
<button class="link-ctrl-btn" data-action="nudge" title="Force relay to next agent">${SVG_SKIP}</button>
|
|
326
|
+
${isRunning
|
|
327
|
+
? `<button class="link-ctrl-btn" data-action="pause" title="Pause">${SVG_PAUSE}</button>`
|
|
328
|
+
: `<button class="link-ctrl-btn" data-action="resume" title="Resume">${SVG_PLAY}</button>`
|
|
329
|
+
}
|
|
330
|
+
<button class="link-ctrl-btn" data-action="copy" title="Copy conversation">${SVG_COPY}</button>
|
|
331
|
+
<button class="link-ctrl-btn danger" data-action="destroy" title="Destroy link">${SVG_UNLINK}</button>
|
|
332
|
+
</div>
|
|
333
|
+
`;
|
|
334
|
+
|
|
335
|
+
// Wire control actions
|
|
336
|
+
el.querySelector('[data-action="toggle-auto"]').onclick = async () => {
|
|
337
|
+
try {
|
|
338
|
+
await updateTerminalLink(link.id, { autoContinue: !autoContinue });
|
|
339
|
+
await _loadLink(link.id);
|
|
340
|
+
} catch (err) { console.error('[link]', err); }
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
el.querySelector('[data-action="nudge"]').onclick = async () => {
|
|
344
|
+
try { await nudgeLink(link.id); } catch (err) { console.error('[link]', err); }
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const pauseResumeBtn = el.querySelector('[data-action="pause"]') || el.querySelector('[data-action="resume"]');
|
|
348
|
+
if (pauseResumeBtn) {
|
|
349
|
+
pauseResumeBtn.onclick = async () => {
|
|
350
|
+
try {
|
|
351
|
+
if (isRunning) await pauseLink(link.id);
|
|
352
|
+
else await resumeLink(link.id);
|
|
353
|
+
await _loadLink(link.id);
|
|
354
|
+
} catch (err) { console.error('[link]', err); }
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
el.querySelector('[data-action="copy"]').onclick = () => _copyConversation();
|
|
359
|
+
|
|
360
|
+
el.querySelector('[data-action="destroy"]').onclick = async () => {
|
|
361
|
+
try {
|
|
362
|
+
await deleteTerminalLink(link.id);
|
|
363
|
+
_activeLink = null;
|
|
364
|
+
_history = [];
|
|
365
|
+
state.linkedSessionIds.clear();
|
|
366
|
+
emit('terminal:tabs-changed');
|
|
367
|
+
_showSetupView();
|
|
368
|
+
} catch (err) { console.error('[link]', err); }
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function _renderTargetSelector(link) {
|
|
373
|
+
const el = _panel.querySelector('.link-panel-target');
|
|
374
|
+
el.innerHTML = `<span class="link-target-label">Send to:</span>` +
|
|
375
|
+
link.sessions.map((s, i) => {
|
|
376
|
+
const meta = PROFILE_META[s.profile] || { label: s.profile, color: '#888' };
|
|
377
|
+
return `<button class="link-target-btn ${i === 0 ? 'selected' : ''}" data-idx="${i}" style="--agent-color:${meta.color}">${meta.label}</button>`;
|
|
378
|
+
}).join('');
|
|
379
|
+
|
|
380
|
+
el.querySelectorAll('.link-target-btn').forEach(btn => {
|
|
381
|
+
btn.onclick = () => {
|
|
382
|
+
el.querySelectorAll('.link-target-btn').forEach(b => b.classList.remove('selected'));
|
|
383
|
+
btn.classList.add('selected');
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function _renderMessages() {
|
|
389
|
+
const container = _panel.querySelector('.link-panel-messages');
|
|
390
|
+
if (!_history.length) {
|
|
391
|
+
container.innerHTML = `
|
|
392
|
+
<div class="link-panel-empty">
|
|
393
|
+
<p style="opacity:0.5;">Link established. Send a message to start the conversation.</p>
|
|
394
|
+
</div>
|
|
395
|
+
`;
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
container.innerHTML = _history.map(msg => {
|
|
400
|
+
const isUser = msg.role === 'user';
|
|
401
|
+
const meta = isUser ? { label: 'You', color: '#4fc3f7' } : (PROFILE_META[msg.role] || { label: msg.role, color: '#888' });
|
|
402
|
+
const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
403
|
+
|
|
404
|
+
return `
|
|
405
|
+
<div class="link-msg ${isUser ? 'link-msg-user' : 'link-msg-agent'}">
|
|
406
|
+
<div class="link-msg-header">
|
|
407
|
+
<span class="link-msg-dot" style="background:${meta.color}"></span>
|
|
408
|
+
<span class="link-msg-name">${meta.label}</span>
|
|
409
|
+
<span class="link-msg-time">${time}</span>
|
|
410
|
+
</div>
|
|
411
|
+
<div class="link-msg-content">${_escapeHtml(msg.content)}</div>
|
|
412
|
+
</div>
|
|
413
|
+
`;
|
|
414
|
+
}).join('');
|
|
415
|
+
|
|
416
|
+
// Scroll to bottom
|
|
417
|
+
container.scrollTop = container.scrollHeight;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Message Sending ──
|
|
421
|
+
|
|
422
|
+
async function _handleSend() {
|
|
423
|
+
if (!_activeLink) return;
|
|
424
|
+
const textarea = _panel.querySelector('.link-panel-input');
|
|
425
|
+
const message = textarea.value.trim();
|
|
426
|
+
if (!message) return;
|
|
427
|
+
|
|
428
|
+
const targetBtn = _panel.querySelector('.link-target-btn.selected');
|
|
429
|
+
const targetIdx = targetBtn ? parseInt(targetBtn.dataset.idx, 10) : undefined;
|
|
430
|
+
|
|
431
|
+
textarea.value = '';
|
|
432
|
+
textarea.style.height = 'auto';
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
await sendLinkMessage(_activeLink, message, targetIdx);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error('[link] Send failed:', err);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Sync Event Handlers ──
|
|
442
|
+
|
|
443
|
+
function _onSyncChunk(msg) {
|
|
444
|
+
if (!_activeLink || msg.linkId !== _activeLink) return;
|
|
445
|
+
const container = _panel?.querySelector('.link-panel-messages');
|
|
446
|
+
if (!container) return;
|
|
447
|
+
|
|
448
|
+
// Find or create the streaming element for this agent
|
|
449
|
+
let streamEl = container.querySelector('.link-msg-streaming');
|
|
450
|
+
if (!streamEl) {
|
|
451
|
+
// Remove "empty" placeholder if present
|
|
452
|
+
const empty = container.querySelector('.link-panel-empty');
|
|
453
|
+
if (empty) empty.remove();
|
|
454
|
+
|
|
455
|
+
const meta = PROFILE_META[msg.profile] || { label: msg.profile, color: '#888' };
|
|
456
|
+
streamEl = document.createElement('div');
|
|
457
|
+
streamEl.className = 'link-msg link-msg-agent link-msg-streaming';
|
|
458
|
+
streamEl.innerHTML = `
|
|
459
|
+
<div class="link-msg-header">
|
|
460
|
+
<span class="link-msg-dot" style="background:${meta.color}"></span>
|
|
461
|
+
<span class="link-msg-name">${meta.label}</span>
|
|
462
|
+
<span class="link-msg-time">streaming...</span>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="link-msg-content"></div>
|
|
465
|
+
`;
|
|
466
|
+
container.appendChild(streamEl);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Append chunk text
|
|
470
|
+
const contentEl = streamEl.querySelector('.link-msg-content');
|
|
471
|
+
contentEl.innerHTML += _escapeHtml(msg.content);
|
|
472
|
+
|
|
473
|
+
// Auto-scroll
|
|
474
|
+
container.scrollTop = container.scrollHeight;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function _onSyncMessage(msg) {
|
|
478
|
+
if (!_activeLink || msg.linkId !== _activeLink) return;
|
|
479
|
+
|
|
480
|
+
// Remove streaming element — the final message replaces it
|
|
481
|
+
_panel?.querySelector('.link-msg-streaming')?.remove();
|
|
482
|
+
|
|
483
|
+
_history.push({ role: msg.role, sessionId: msg.sessionId, content: msg.content, timestamp: msg.timestamp });
|
|
484
|
+
_renderMessages();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function _onSyncError(msg) {
|
|
488
|
+
if (!_activeLink || msg.linkId !== _activeLink) return;
|
|
489
|
+
const container = _panel?.querySelector('.link-panel-messages');
|
|
490
|
+
if (!container) return;
|
|
491
|
+
|
|
492
|
+
// Remove streaming element
|
|
493
|
+
container.querySelector('.link-msg-streaming')?.remove();
|
|
494
|
+
|
|
495
|
+
// Show error inline
|
|
496
|
+
const errorEl = document.createElement('div');
|
|
497
|
+
errorEl.className = 'link-msg link-msg-error';
|
|
498
|
+
errorEl.innerHTML = `
|
|
499
|
+
<div class="link-msg-header">
|
|
500
|
+
<span class="link-msg-dot" style="background:#f44336"></span>
|
|
501
|
+
<span class="link-msg-name">Error</span>
|
|
502
|
+
</div>
|
|
503
|
+
<div class="link-msg-content" style="color:#f44336">${_escapeHtml(msg.error || 'Unknown error')}</div>
|
|
504
|
+
`;
|
|
505
|
+
container.appendChild(errorEl);
|
|
506
|
+
container.scrollTop = container.scrollHeight;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function _onSyncAgentStarted(msg) {
|
|
510
|
+
if (!_activeLink || msg.linkId !== _activeLink) return;
|
|
511
|
+
// Update agent indicators
|
|
512
|
+
const agents = _panel?.querySelectorAll('.link-agent');
|
|
513
|
+
if (!agents) return;
|
|
514
|
+
agents.forEach(el => {
|
|
515
|
+
const idx = parseInt(el.dataset.idx, 10);
|
|
516
|
+
const session = el.querySelector('.link-agent-name')?.textContent;
|
|
517
|
+
if (PROFILE_META[msg.profile]?.label === session || idx === msg.activeAgent) {
|
|
518
|
+
el.classList.add('active');
|
|
519
|
+
if (!el.querySelector('.link-agent-typing')) {
|
|
520
|
+
el.insertAdjacentHTML('beforeend', '<span class="link-agent-typing">typing...</span>');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function _onSyncAgentFinished(msg) {
|
|
527
|
+
if (!_activeLink || msg.linkId !== _activeLink) return;
|
|
528
|
+
// Remove typing indicators
|
|
529
|
+
_panel?.querySelectorAll('.link-agent').forEach(el => {
|
|
530
|
+
el.classList.remove('active');
|
|
531
|
+
el.querySelector('.link-agent-typing')?.remove();
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function _onSyncLinkCreated(msg) {
|
|
536
|
+
// Update linked session IDs for tab badges
|
|
537
|
+
if (msg.sessions) {
|
|
538
|
+
msg.sessions.forEach(sid => state.linkedSessionIds.add(sid));
|
|
539
|
+
emit('terminal:tabs-changed');
|
|
540
|
+
}
|
|
541
|
+
// Update panel if open
|
|
542
|
+
if (_visible && !_activeLink) {
|
|
543
|
+
_activeLink = msg.linkId;
|
|
544
|
+
_loadLink(msg.linkId);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function _onSyncLinkDeleted(msg) {
|
|
549
|
+
// Clear linked session IDs for tab badges
|
|
550
|
+
if (msg.sessions) {
|
|
551
|
+
msg.sessions.forEach(sid => state.linkedSessionIds.delete(sid));
|
|
552
|
+
} else {
|
|
553
|
+
// If no session list provided, clear all (we can refresh from server)
|
|
554
|
+
state.linkedSessionIds.clear();
|
|
555
|
+
}
|
|
556
|
+
emit('terminal:tabs-changed');
|
|
557
|
+
|
|
558
|
+
if (_activeLink === msg.linkId) {
|
|
559
|
+
_activeLink = null;
|
|
560
|
+
_history = [];
|
|
561
|
+
if (_visible) _showSetupView();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function _onSyncPaused() {
|
|
566
|
+
if (_activeLink && _visible) _loadLink(_activeLink);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function _onSyncResumed() {
|
|
570
|
+
if (_activeLink && _visible) _loadLink(_activeLink);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ── Drag & Resize ──
|
|
574
|
+
|
|
575
|
+
function _onDragStart(e) {
|
|
576
|
+
if (e.target.closest('.link-panel-btn')) return;
|
|
577
|
+
e.preventDefault();
|
|
578
|
+
const rect = _panel.getBoundingClientRect();
|
|
579
|
+
_panel.style.transform = 'none';
|
|
580
|
+
_dragState = { startX: e.clientX, startY: e.clientY, startLeft: rect.left, startTop: rect.top };
|
|
581
|
+
_panel.classList.add('dragging');
|
|
582
|
+
document.addEventListener('mousemove', _onDragMove);
|
|
583
|
+
document.addEventListener('mouseup', _onDragEnd);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function _onDragMove(e) {
|
|
587
|
+
if (!_dragState) return;
|
|
588
|
+
const dx = e.clientX - _dragState.startX;
|
|
589
|
+
const dy = e.clientY - _dragState.startY;
|
|
590
|
+
_panel.style.left = (_dragState.startLeft + dx) + 'px';
|
|
591
|
+
_panel.style.top = (_dragState.startTop + dy) + 'px';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function _onDragEnd() {
|
|
595
|
+
_dragState = null;
|
|
596
|
+
_panel.classList.remove('dragging');
|
|
597
|
+
document.removeEventListener('mousemove', _onDragMove);
|
|
598
|
+
document.removeEventListener('mouseup', _onDragEnd);
|
|
599
|
+
_savePosition();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function _onResizeHover(e) {
|
|
603
|
+
if (_resizeState) return;
|
|
604
|
+
const rect = _panel.getBoundingClientRect();
|
|
605
|
+
const edge = 6;
|
|
606
|
+
const x = e.clientX - rect.left;
|
|
607
|
+
const y = e.clientY - rect.top;
|
|
608
|
+
const onLeft = x < edge, onRight = x > rect.width - edge;
|
|
609
|
+
const onTop = y < edge, onBottom = y > rect.height - edge;
|
|
610
|
+
|
|
611
|
+
if ((onLeft && onTop) || (onRight && onBottom)) _panel.style.cursor = 'nwse-resize';
|
|
612
|
+
else if ((onRight && onTop) || (onLeft && onBottom)) _panel.style.cursor = 'nesw-resize';
|
|
613
|
+
else if (onLeft || onRight) _panel.style.cursor = 'ew-resize';
|
|
614
|
+
else if (onTop || onBottom) _panel.style.cursor = 'ns-resize';
|
|
615
|
+
else _panel.style.cursor = '';
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function _onResizeStart(e) {
|
|
619
|
+
if (e.target.closest('.link-panel-header') || e.target.closest('.link-panel-body') || e.target.closest('.link-panel-input-area')) return;
|
|
620
|
+
const rect = _panel.getBoundingClientRect();
|
|
621
|
+
const edge = 6;
|
|
622
|
+
const x = e.clientX - rect.left;
|
|
623
|
+
const y = e.clientY - rect.top;
|
|
624
|
+
const dirs = {
|
|
625
|
+
left: x < edge, right: x > rect.width - edge,
|
|
626
|
+
top: y < edge, bottom: y > rect.height - edge,
|
|
627
|
+
};
|
|
628
|
+
if (!dirs.left && !dirs.right && !dirs.top && !dirs.bottom) return;
|
|
629
|
+
e.preventDefault();
|
|
630
|
+
_resizeState = { dirs, startX: e.clientX, startY: e.clientY, startRect: rect };
|
|
631
|
+
document.addEventListener('mousemove', _onResizeMove);
|
|
632
|
+
document.addEventListener('mouseup', _onResizeEnd);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function _onResizeMove(e) {
|
|
636
|
+
if (!_resizeState) return;
|
|
637
|
+
const { dirs, startX, startY, startRect } = _resizeState;
|
|
638
|
+
const dx = e.clientX - startX, dy = e.clientY - startY;
|
|
639
|
+
const minW = 320, minH = 300;
|
|
640
|
+
if (dirs.right) _panel.style.width = Math.max(minW, startRect.width + dx) + 'px';
|
|
641
|
+
if (dirs.bottom) _panel.style.height = Math.max(minH, startRect.height + dy) + 'px';
|
|
642
|
+
if (dirs.left) {
|
|
643
|
+
const newW = Math.max(minW, startRect.width - dx);
|
|
644
|
+
_panel.style.width = newW + 'px';
|
|
645
|
+
_panel.style.left = (startRect.left + startRect.width - newW) + 'px';
|
|
646
|
+
}
|
|
647
|
+
if (dirs.top) {
|
|
648
|
+
const newH = Math.max(minH, startRect.height - dy);
|
|
649
|
+
_panel.style.height = newH + 'px';
|
|
650
|
+
_panel.style.top = (startRect.top + startRect.height - newH) + 'px';
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function _onResizeEnd() {
|
|
655
|
+
_resizeState = null;
|
|
656
|
+
document.removeEventListener('mousemove', _onResizeMove);
|
|
657
|
+
document.removeEventListener('mouseup', _onResizeEnd);
|
|
658
|
+
_savePosition();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function _minimize() {
|
|
662
|
+
_minimized = !_minimized;
|
|
663
|
+
if (_minimized) {
|
|
664
|
+
_panel.classList.add('minimized');
|
|
665
|
+
} else {
|
|
666
|
+
_panel.classList.remove('minimized');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function _savePosition() {
|
|
671
|
+
if (!_panel) return;
|
|
672
|
+
const rect = _panel.getBoundingClientRect();
|
|
673
|
+
storage.setItem(KEYS.LINK_PANEL_POS, JSON.stringify({ x: Math.round(rect.left), y: Math.round(rect.top) }));
|
|
674
|
+
storage.setItem(KEYS.LINK_PANEL_SIZE, JSON.stringify({ w: Math.round(rect.width), h: Math.round(rect.height) }));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ── Utilities ──
|
|
678
|
+
|
|
679
|
+
function _escapeHtml(str) {
|
|
680
|
+
const div = document.createElement('div');
|
|
681
|
+
div.textContent = str;
|
|
682
|
+
return div.innerHTML.replace(/\n/g, '<br>');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function _copyConversation() {
|
|
686
|
+
if (!_history.length) return;
|
|
687
|
+
const text = _history.map(msg => {
|
|
688
|
+
const label = msg.role === 'user' ? 'You' : (PROFILE_META[msg.role]?.label || msg.role);
|
|
689
|
+
return `[${label}]\n${msg.content}`;
|
|
690
|
+
}).join('\n\n---\n\n');
|
|
691
|
+
|
|
692
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
693
|
+
const btn = _panel.querySelector('[data-action="copy"]');
|
|
694
|
+
if (btn) {
|
|
695
|
+
btn.style.color = '#74c7a5';
|
|
696
|
+
setTimeout(() => { btn.style.color = ''; }, 1500);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ── Init ──
|
|
702
|
+
|
|
703
|
+
export async function initLink() {
|
|
704
|
+
console.log('[link-panel] initLink called');
|
|
705
|
+
|
|
706
|
+
// Listen for sync events
|
|
707
|
+
on('sync:link:message', _onSyncMessage);
|
|
708
|
+
on('sync:link:chunk', _onSyncChunk);
|
|
709
|
+
on('sync:link:agent-started', _onSyncAgentStarted);
|
|
710
|
+
on('sync:link:agent-finished', _onSyncAgentFinished);
|
|
711
|
+
on('sync:link:created', _onSyncLinkCreated);
|
|
712
|
+
on('sync:link:deleted', _onSyncLinkDeleted);
|
|
713
|
+
on('sync:link:paused', _onSyncPaused);
|
|
714
|
+
on('sync:link:resumed', _onSyncResumed);
|
|
715
|
+
on('sync:link:error', _onSyncError);
|
|
716
|
+
|
|
717
|
+
// Expose for external triggers (menubar, keybind, etc.)
|
|
718
|
+
on('link:open', () => { console.log('[link-panel] link:open event'); openLinkPanel(); });
|
|
719
|
+
on('link:toggle', () => { console.log('[link-panel] link:toggle event'); toggleLinkPanel(); });
|
|
720
|
+
|
|
721
|
+
// Load existing links on startup to populate tab badges
|
|
722
|
+
try {
|
|
723
|
+
const { links } = await fetchTerminalLinks();
|
|
724
|
+
if (links.length > 0) {
|
|
725
|
+
for (const link of links) {
|
|
726
|
+
link.sessions.forEach(s => state.linkedSessionIds.add(s.id));
|
|
727
|
+
}
|
|
728
|
+
emit('terminal:tabs-changed');
|
|
729
|
+
}
|
|
730
|
+
} catch { /* server may not be ready yet */ }
|
|
731
|
+
}
|