@agent-link/server 0.1.89 → 0.1.90

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-link/server",
3
- "version": "0.1.89",
3
+ "version": "0.1.90",
4
4
  "description": "AgentLink relay server",
5
5
  "license": "MIT",
6
6
  "repository": {
package/web/app.js CHANGED
@@ -154,7 +154,7 @@ const App = {
154
154
  hostname, workdirHistory,
155
155
  });
156
156
 
157
- const { connect, wsSend, closeWs, submitPassword } = createConnection({
157
+ const { connect, wsSend, closeWs, submitPassword, setDequeueNext } = createConnection({
158
158
  status, agentName, hostname, workDir, sessionId, error,
159
159
  serverVersion, agentVersion,
160
160
  messages, isProcessing, isCompacting, visibleLimit,
@@ -166,10 +166,12 @@ const App = {
166
166
 
167
167
  // Now wire up the forwarding function
168
168
  _wsSend = wsSend;
169
+ setDequeueNext(dequeueNext);
169
170
 
170
171
  // ── Computed ──
172
+ const hasInput = computed(() => !!(inputText.value.trim() || attachments.value.length > 0));
171
173
  const canSend = computed(() =>
172
- status.value === 'Connected' && (inputText.value.trim() || attachments.value.length > 0) && !isProcessing.value && !isCompacting.value
174
+ status.value === 'Connected' && hasInput.value && !isCompacting.value
173
175
  && !messages.value.some(m => m.role === 'ask-question' && !m.answered)
174
176
  );
175
177
 
@@ -195,15 +197,6 @@ const App = {
195
197
  name: f.name, size: f.size, isImage: f.isImage, thumbUrl: f.thumbUrl,
196
198
  }));
197
199
 
198
- messages.value.push({
199
- id: streaming.nextId(), role: 'user',
200
- content: text || (files.length > 0 ? `[${files.length} file${files.length > 1 ? 's' : ''} attached]` : ''),
201
- attachments: msgAttachments.length > 0 ? msgAttachments : undefined,
202
- timestamp: new Date(),
203
- });
204
- isProcessing.value = true;
205
- scrollToBottom(true);
206
-
207
200
  const payload = { type: 'chat', prompt: text || '(see attached files)' };
208
201
  if (needsResume.value && currentClaudeSessionId.value) {
209
202
  payload.resumeSessionId = currentClaudeSessionId.value;
@@ -214,7 +207,25 @@ const App = {
214
207
  name: f.name, mimeType: f.mimeType, data: f.data,
215
208
  }));
216
209
  }
217
- wsSend(payload);
210
+
211
+ const userMsg = {
212
+ id: streaming.nextId(), role: 'user',
213
+ content: text || (files.length > 0 ? `[${files.length} file${files.length > 1 ? 's' : ''} attached]` : ''),
214
+ attachments: msgAttachments.length > 0 ? msgAttachments : undefined,
215
+ timestamp: new Date(),
216
+ };
217
+
218
+ if (isProcessing.value) {
219
+ userMsg.status = 'queued';
220
+ userMsg._queuedPayload = payload;
221
+ messages.value.push(userMsg);
222
+ } else {
223
+ userMsg.status = 'sent';
224
+ messages.value.push(userMsg);
225
+ isProcessing.value = true;
226
+ wsSend(payload);
227
+ }
228
+ scrollToBottom(true);
218
229
  attachments.value = [];
219
230
  }
220
231
 
@@ -223,6 +234,22 @@ const App = {
223
234
  wsSend({ type: 'cancel_execution' });
224
235
  }
225
236
 
237
+ function dequeueNext() {
238
+ const idx = messages.value.findIndex(m => m.status === 'queued');
239
+ if (idx === -1) return;
240
+ const msg = messages.value[idx];
241
+ msg.status = 'sent';
242
+ isProcessing.value = true;
243
+ wsSend(msg._queuedPayload);
244
+ delete msg._queuedPayload;
245
+ scrollToBottom(true);
246
+ }
247
+
248
+ function removeQueuedMessage(msgId) {
249
+ const idx = messages.value.findIndex(m => m.id === msgId);
250
+ if (idx !== -1) messages.value.splice(idx, 1);
251
+ }
252
+
226
253
  function handleKeydown(e) {
227
254
  if (e.key === 'Enter' && !e.shiftKey) {
228
255
  e.preventDefault();
@@ -256,8 +283,8 @@ const App = {
256
283
  status, agentName, hostname, workDir, sessionId, error,
257
284
  serverVersion, agentVersion,
258
285
  messages, visibleMessages, hasMoreMessages, loadMoreMessages,
259
- inputText, isProcessing, isCompacting, canSend, inputRef,
260
- sendMessage, handleKeydown, cancelExecution, onMessageListScroll,
286
+ inputText, isProcessing, isCompacting, canSend, hasInput, inputRef,
287
+ sendMessage, handleKeydown, cancelExecution, removeQueuedMessage, onMessageListScroll,
261
288
  getRenderedContent, copyMessage, toggleTool,
262
289
  isPrevAssistant: _isPrevAssistant,
263
290
  toggleContextSummary, formatTimestamp,
@@ -457,7 +484,8 @@ const App = {
457
484
  <!-- User message -->
458
485
  <template v-if="msg.role === 'user'">
459
486
  <div class="message-role-label user-label">You</div>
460
- <div class="message-bubble user-bubble" :title="formatTimestamp(msg.timestamp)">
487
+ <div :class="['message-bubble', 'user-bubble', { queued: msg.status === 'queued' }]" :title="formatTimestamp(msg.timestamp)">
488
+ <button v-if="msg.status === 'queued'" class="queue-remove-btn" @click="removeQueuedMessage(msg.id)" title="Remove from queue">&times;</button>
461
489
  <div class="message-content">{{ msg.content }}</div>
462
490
  <div v-if="msg.attachments && msg.attachments.length" class="message-attachments">
463
491
  <div v-for="(att, ai) in msg.attachments" :key="ai" class="message-attachment-chip">
@@ -468,6 +496,7 @@ const App = {
468
496
  <span>{{ att.name }}</span>
469
497
  </div>
470
498
  </div>
499
+ <div v-if="msg.status === 'queued'" class="queue-badge">queued</div>
471
500
  </div>
472
501
  </template>
473
502
 
@@ -620,7 +649,7 @@ const App = {
620
649
  <button class="attach-btn" @click="triggerFileInput" :disabled="status !== 'Connected' || isCompacting || attachments.length >= 5" title="Attach files">
621
650
  <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5a2.5 2.5 0 0 0 5 0V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>
622
651
  </button>
623
- <button v-if="isProcessing" @click="cancelExecution" class="send-btn stop-btn" title="Stop generation">
652
+ <button v-if="isProcessing && !hasInput" @click="cancelExecution" class="send-btn stop-btn" title="Stop generation">
624
653
  <svg viewBox="0 0 24 24" width="14" height="14"><rect x="6" y="6" width="12" height="12" rx="2" fill="currentColor"/></svg>
625
654
  </button>
626
655
  <button v-else @click="sendMessage" :disabled="!canSend" class="send-btn" title="Send">
@@ -22,6 +22,10 @@ export function createConnection(deps) {
22
22
  scrollToBottom,
23
23
  } = deps;
24
24
 
25
+ // Dequeue callback — set after creation to resolve circular dependency
26
+ let _dequeueNext = () => {};
27
+ function setDequeueNext(fn) { _dequeueNext = fn; }
28
+
25
29
  let ws = null;
26
30
  let sessionKey = null;
27
31
  let reconnectAttempts = 0;
@@ -226,6 +230,7 @@ export function createConnection(deps) {
226
230
  scrollToBottom();
227
231
  isProcessing.value = false;
228
232
  isCompacting.value = false;
233
+ _dequeueNext();
229
234
  } else if (msg.type === 'claude_output') {
230
235
  handleClaudeOutput(msg, scheduleHighlight);
231
236
  } else if (msg.type === 'command_output') {
@@ -269,6 +274,7 @@ export function createConnection(deps) {
269
274
  });
270
275
  scrollToBottom();
271
276
  }
277
+ _dequeueNext();
272
278
  } else if (msg.type === 'ask_user_question') {
273
279
  streaming.flushReveal();
274
280
  finalizeStreamingMsg(scheduleHighlight);
@@ -446,5 +452,5 @@ export function createConnection(deps) {
446
452
  ws.send(JSON.stringify({ type: 'authenticate', password: pwd }));
447
453
  }
448
454
 
449
- return { connect, wsSend, closeWs, submitPassword };
455
+ return { connect, wsSend, closeWs, submitPassword, setDequeueNext };
450
456
  }
package/web/style.css CHANGED
@@ -787,6 +787,46 @@ body {
787
787
  color: var(--text-primary);
788
788
  }
789
789
 
790
+ /* ── Queued message (pending send) ── */
791
+ .user-bubble.queued {
792
+ background: transparent;
793
+ border: 1px dashed var(--border);
794
+ opacity: 0.7;
795
+ }
796
+
797
+ .queue-badge {
798
+ font-size: 0.65rem;
799
+ font-weight: 600;
800
+ text-transform: uppercase;
801
+ letter-spacing: 0.05em;
802
+ color: var(--text-secondary);
803
+ text-align: right;
804
+ margin-top: 0.2rem;
805
+ }
806
+
807
+ .queue-remove-btn {
808
+ position: absolute;
809
+ top: 4px;
810
+ right: 6px;
811
+ background: none;
812
+ border: none;
813
+ color: var(--text-secondary);
814
+ font-size: 1rem;
815
+ line-height: 1;
816
+ cursor: pointer;
817
+ padding: 0 2px;
818
+ opacity: 0;
819
+ transition: color 0.15s, opacity 0.15s;
820
+ }
821
+
822
+ .user-bubble.queued:hover .queue-remove-btn {
823
+ opacity: 1;
824
+ }
825
+
826
+ .queue-remove-btn:hover {
827
+ color: var(--error);
828
+ }
829
+
790
830
  .assistant-bubble {
791
831
  background: transparent;
792
832
  padding: 0.2rem 0;