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.
Files changed (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. 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
+ }