@agent-link/server 0.1.89 → 0.1.91

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.91",
4
4
  "description": "AgentLink relay server",
5
5
  "license": "MIT",
6
6
  "repository": {
package/web/app.js CHANGED
@@ -47,6 +47,7 @@ const App = {
47
47
  const inputText = ref('');
48
48
  const isProcessing = ref(false);
49
49
  const isCompacting = ref(false);
50
+ const queuedMessages = ref([]);
50
51
  const inputRef = ref(null);
51
52
 
52
53
  // Sidebar state
@@ -154,10 +155,10 @@ const App = {
154
155
  hostname, workdirHistory,
155
156
  });
156
157
 
157
- const { connect, wsSend, closeWs, submitPassword } = createConnection({
158
+ const { connect, wsSend, closeWs, submitPassword, setDequeueNext } = createConnection({
158
159
  status, agentName, hostname, workDir, sessionId, error,
159
160
  serverVersion, agentVersion,
160
- messages, isProcessing, isCompacting, visibleLimit,
161
+ messages, isProcessing, isCompacting, visibleLimit, queuedMessages,
161
162
  historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
162
163
  folderPickerLoading, folderPickerEntries, folderPickerPath,
163
164
  authRequired, authPassword, authError, authAttempts, authLocked,
@@ -166,10 +167,12 @@ const App = {
166
167
 
167
168
  // Now wire up the forwarding function
168
169
  _wsSend = wsSend;
170
+ setDequeueNext(dequeueNext);
169
171
 
170
172
  // ── Computed ──
173
+ const hasInput = computed(() => !!(inputText.value.trim() || attachments.value.length > 0));
171
174
  const canSend = computed(() =>
172
- status.value === 'Connected' && (inputText.value.trim() || attachments.value.length > 0) && !isProcessing.value && !isCompacting.value
175
+ status.value === 'Connected' && hasInput.value && !isCompacting.value
173
176
  && !messages.value.some(m => m.role === 'ask-question' && !m.answered)
174
177
  );
175
178
 
@@ -195,15 +198,6 @@ const App = {
195
198
  name: f.name, size: f.size, isImage: f.isImage, thumbUrl: f.thumbUrl,
196
199
  }));
197
200
 
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
201
  const payload = { type: 'chat', prompt: text || '(see attached files)' };
208
202
  if (needsResume.value && currentClaudeSessionId.value) {
209
203
  payload.resumeSessionId = currentClaudeSessionId.value;
@@ -214,7 +208,23 @@ const App = {
214
208
  name: f.name, mimeType: f.mimeType, data: f.data,
215
209
  }));
216
210
  }
217
- wsSend(payload);
211
+
212
+ const userMsg = {
213
+ id: streaming.nextId(), role: 'user',
214
+ content: text || (files.length > 0 ? `[${files.length} file${files.length > 1 ? 's' : ''} attached]` : ''),
215
+ attachments: msgAttachments.length > 0 ? msgAttachments : undefined,
216
+ timestamp: new Date(),
217
+ };
218
+
219
+ if (isProcessing.value) {
220
+ queuedMessages.value.push({ id: streaming.nextId(), content: userMsg.content, attachments: userMsg.attachments, payload });
221
+ } else {
222
+ userMsg.status = 'sent';
223
+ messages.value.push(userMsg);
224
+ isProcessing.value = true;
225
+ wsSend(payload);
226
+ }
227
+ scrollToBottom(true);
218
228
  attachments.value = [];
219
229
  }
220
230
 
@@ -223,6 +233,25 @@ const App = {
223
233
  wsSend({ type: 'cancel_execution' });
224
234
  }
225
235
 
236
+ function dequeueNext() {
237
+ if (queuedMessages.value.length === 0) return;
238
+ const queued = queuedMessages.value.shift();
239
+ const userMsg = {
240
+ id: queued.id, role: 'user', status: 'sent',
241
+ content: queued.content, attachments: queued.attachments,
242
+ timestamp: new Date(),
243
+ };
244
+ messages.value.push(userMsg);
245
+ isProcessing.value = true;
246
+ wsSend(queued.payload);
247
+ scrollToBottom(true);
248
+ }
249
+
250
+ function removeQueuedMessage(msgId) {
251
+ const idx = queuedMessages.value.findIndex(m => m.id === msgId);
252
+ if (idx !== -1) queuedMessages.value.splice(idx, 1);
253
+ }
254
+
226
255
  function handleKeydown(e) {
227
256
  if (e.key === 'Enter' && !e.shiftKey) {
228
257
  e.preventDefault();
@@ -256,8 +285,8 @@ const App = {
256
285
  status, agentName, hostname, workDir, sessionId, error,
257
286
  serverVersion, agentVersion,
258
287
  messages, visibleMessages, hasMoreMessages, loadMoreMessages,
259
- inputText, isProcessing, isCompacting, canSend, inputRef,
260
- sendMessage, handleKeydown, cancelExecution, onMessageListScroll,
288
+ inputText, isProcessing, isCompacting, canSend, hasInput, inputRef, queuedMessages,
289
+ sendMessage, handleKeydown, cancelExecution, removeQueuedMessage, onMessageListScroll,
261
290
  getRenderedContent, copyMessage, toggleTool,
262
291
  isPrevAssistant: _isPrevAssistant,
263
292
  toggleContextSummary, formatTimestamp,
@@ -587,6 +616,17 @@ const App = {
587
616
  @change="handleFileSelect"
588
617
  accept="image/*,text/*,.pdf,.json,.md,.py,.js,.ts,.tsx,.jsx,.css,.html,.xml,.yaml,.yml,.toml,.sh,.sql,.csv"
589
618
  />
619
+ <div v-if="queuedMessages.length > 0" class="queue-bar">
620
+ <div v-for="(qm, qi) in queuedMessages" :key="qm.id" class="queue-item">
621
+ <span class="queue-item-num">{{ qi + 1 }}.</span>
622
+ <span class="queue-item-text">{{ qm.content }}</span>
623
+ <span v-if="qm.attachments && qm.attachments.length" class="queue-item-attach" :title="qm.attachments.map(a => a.name).join(', ')">
624
+ <svg viewBox="0 0 24 24" width="11" height="11"><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>
625
+ {{ qm.attachments.length }}
626
+ </span>
627
+ <button class="queue-item-remove" @click="removeQueuedMessage(qm.id)" title="Remove from queue">&times;</button>
628
+ </div>
629
+ </div>
590
630
  <div
591
631
  :class="['input-card', { 'drag-over': dragOver }]"
592
632
  @dragover="handleDragOver"
@@ -620,7 +660,7 @@ const App = {
620
660
  <button class="attach-btn" @click="triggerFileInput" :disabled="status !== 'Connected' || isCompacting || attachments.length >= 5" title="Attach files">
621
661
  <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
662
  </button>
623
- <button v-if="isProcessing" @click="cancelExecution" class="send-btn stop-btn" title="Stop generation">
663
+ <button v-if="isProcessing && !hasInput" @click="cancelExecution" class="send-btn stop-btn" title="Stop generation">
624
664
  <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
665
  </button>
626
666
  <button v-else @click="sendMessage" :disabled="!canSend" class="send-btn" title="Send">
@@ -14,7 +14,7 @@ export function createConnection(deps) {
14
14
  const {
15
15
  status, agentName, hostname, workDir, sessionId, error,
16
16
  serverVersion, agentVersion,
17
- messages, isProcessing, isCompacting, visibleLimit,
17
+ messages, isProcessing, isCompacting, visibleLimit, queuedMessages,
18
18
  historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
19
19
  folderPickerLoading, folderPickerEntries, folderPickerPath,
20
20
  authRequired, authPassword, authError, authAttempts, authLocked,
@@ -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;
@@ -203,6 +207,7 @@ export function createConnection(deps) {
203
207
  error.value = 'Agent disconnected. Waiting for reconnect...';
204
208
  isProcessing.value = false;
205
209
  isCompacting.value = false;
210
+ queuedMessages.value = [];
206
211
  } else if (msg.type === 'agent_reconnected') {
207
212
  status.value = 'Connected';
208
213
  error.value = '';
@@ -226,6 +231,7 @@ export function createConnection(deps) {
226
231
  scrollToBottom();
227
232
  isProcessing.value = false;
228
233
  isCompacting.value = false;
234
+ _dequeueNext();
229
235
  } else if (msg.type === 'claude_output') {
230
236
  handleClaudeOutput(msg, scheduleHighlight);
231
237
  } else if (msg.type === 'command_output') {
@@ -269,6 +275,7 @@ export function createConnection(deps) {
269
275
  });
270
276
  scrollToBottom();
271
277
  }
278
+ _dequeueNext();
272
279
  } else if (msg.type === 'ask_user_question') {
273
280
  streaming.flushReveal();
274
281
  finalizeStreamingMsg(scheduleHighlight);
@@ -387,6 +394,7 @@ export function createConnection(deps) {
387
394
  localStorage.setItem(`agentlink-workdir-${sessionId.value}`, msg.workDir);
388
395
  sidebar.addToWorkdirHistory(msg.workDir);
389
396
  messages.value = [];
397
+ queuedMessages.value = [];
390
398
  toolMsgMap.clear();
391
399
  visibleLimit.value = 50;
392
400
  streaming.setMessageIdCounter(0);
@@ -408,6 +416,7 @@ export function createConnection(deps) {
408
416
  const wasConnected = status.value === 'Connected' || status.value === 'Connecting...';
409
417
  isProcessing.value = false;
410
418
  isCompacting.value = false;
419
+ queuedMessages.value = [];
411
420
 
412
421
  // Don't auto-reconnect if auth-locked or still in auth prompt
413
422
  if (authLocked.value || authRequired.value) return;
@@ -446,5 +455,5 @@ export function createConnection(deps) {
446
455
  ws.send(JSON.stringify({ type: 'authenticate', password: pwd }));
447
456
  }
448
457
 
449
- return { connect, wsSend, closeWs, submitPassword };
458
+ return { connect, wsSend, closeWs, submitPassword, setDequeueNext };
450
459
  }
package/web/style.css CHANGED
@@ -787,6 +787,72 @@ body {
787
787
  color: var(--text-primary);
788
788
  }
789
789
 
790
+ /* ── Queue bar (pending messages above input) ── */
791
+ .queue-bar {
792
+ max-width: 768px;
793
+ margin: 0 auto 6px;
794
+ display: flex;
795
+ flex-direction: column;
796
+ gap: 3px;
797
+ }
798
+
799
+ .queue-item {
800
+ display: flex;
801
+ align-items: center;
802
+ gap: 6px;
803
+ background: var(--bg-secondary);
804
+ border: 1px dashed var(--border);
805
+ border-radius: 8px;
806
+ padding: 4px 8px;
807
+ font-size: 0.8rem;
808
+ line-height: 1.3;
809
+ color: var(--text-secondary);
810
+ opacity: 0.85;
811
+ }
812
+
813
+ .queue-item-num {
814
+ flex-shrink: 0;
815
+ font-weight: 600;
816
+ color: var(--text-secondary);
817
+ font-size: 0.7rem;
818
+ opacity: 0.6;
819
+ }
820
+
821
+ .queue-item-text {
822
+ flex: 1;
823
+ min-width: 0;
824
+ overflow: hidden;
825
+ text-overflow: ellipsis;
826
+ white-space: nowrap;
827
+ }
828
+
829
+ .queue-item-attach {
830
+ flex-shrink: 0;
831
+ display: flex;
832
+ align-items: center;
833
+ gap: 2px;
834
+ font-size: 0.7rem;
835
+ opacity: 0.6;
836
+ }
837
+
838
+ .queue-item-remove {
839
+ flex-shrink: 0;
840
+ background: none;
841
+ border: none;
842
+ color: var(--text-secondary);
843
+ font-size: 1rem;
844
+ line-height: 1;
845
+ cursor: pointer;
846
+ padding: 0 2px;
847
+ opacity: 0.5;
848
+ transition: color 0.15s, opacity 0.15s;
849
+ }
850
+
851
+ .queue-item-remove:hover {
852
+ opacity: 1;
853
+ color: var(--error);
854
+ }
855
+
790
856
  .assistant-bubble {
791
857
  background: transparent;
792
858
  padding: 0.2rem 0;