agentgui 1.0.772 → 1.0.774

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/database.js CHANGED
@@ -422,7 +422,8 @@ try {
422
422
  model: 'TEXT',
423
423
  subAgent: 'TEXT',
424
424
  pinned: 'INTEGER DEFAULT 0',
425
- tags: 'TEXT'
425
+ tags: 'TEXT',
426
+ sortOrder: 'INTEGER DEFAULT 0'
426
427
  };
427
428
 
428
429
  let addedColumns = false;
@@ -625,8 +626,8 @@ function generateId(prefix) {
625
626
  return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
626
627
  }
627
628
 
628
- import { createQueries } from './lib/db-queries.js';
629
-
630
- export const queries = createQueries(db, prep, generateId);
629
+ import { createQueries } from './lib/db-queries.js';
630
+
631
+ export const queries = createQueries(db, prep, generateId);
631
632
 
632
633
  export default { queries };
package/lib/db-queries.js CHANGED
@@ -36,16 +36,20 @@ export function createQueries(db, prep, generateId) {
36
36
 
37
37
  getConversationsList() {
38
38
  const stmt = prep(
39
- 'SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model, subAgent, pinned FROM conversations WHERE status NOT IN (?, ?) ORDER BY pinned DESC, updated_at DESC'
39
+ 'SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model, subAgent, pinned, sortOrder FROM conversations WHERE status NOT IN (?, ?) ORDER BY pinned DESC, sortOrder ASC, updated_at DESC'
40
40
  );
41
41
  return stmt.all('deleted', 'archived');
42
42
  },
43
43
 
44
44
  getConversations() {
45
- const stmt = prep('SELECT * FROM conversations WHERE status NOT IN (?, ?) ORDER BY pinned DESC, updated_at DESC');
45
+ const stmt = prep('SELECT * FROM conversations WHERE status NOT IN (?, ?) ORDER BY pinned DESC, sortOrder ASC, updated_at DESC');
46
46
  return stmt.all('deleted', 'archived');
47
47
  },
48
48
 
49
+ updateConversationSortOrder(id, sortOrder) {
50
+ prep('UPDATE conversations SET sortOrder = ? WHERE id = ?').run(sortOrder, id);
51
+ },
52
+
49
53
  getArchivedConversations() {
50
54
  const stmt = prep('SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, model, subAgent FROM conversations WHERE status = ? ORDER BY updated_at DESC');
51
55
  return stmt.all('archived');
@@ -125,6 +125,14 @@ export function register(router, deps) {
125
125
  return { ok: true, chunks: result.chunks, total: result.total, hasMore: result.hasMore, limit: result.limit };
126
126
  });
127
127
 
128
+ router.handle('conv.reorder', (p) => {
129
+ if (!Array.isArray(p.order)) throw Object.assign(new Error('order array required'), { code: 400 });
130
+ for (let i = 0; i < p.order.length; i++) {
131
+ queries.updateConversationSortOrder(p.order[i], i);
132
+ }
133
+ return { ok: true };
134
+ });
135
+
128
136
  router.handle('conv.export', (p) => {
129
137
  const conv = queries.getConversation(p.id);
130
138
  if (!conv) notFound();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.772",
3
+ "version": "1.0.774",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/static/app.js CHANGED
@@ -903,4 +903,66 @@ document.addEventListener('keydown', (e) => {
903
903
  });
904
904
  })();
905
905
 
906
+ (function initPresets() {
907
+ const STORAGE_KEY = 'agentgui-presets';
908
+ const saveBtn = document.getElementById('savePresetBtn');
909
+ const selector = document.getElementById('presetSelector');
910
+ if (!saveBtn || !selector) return;
911
+
912
+ function getPresets() {
913
+ try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch { return []; }
914
+ }
915
+ function savePresets(p) { localStorage.setItem(STORAGE_KEY, JSON.stringify(p)); }
916
+
917
+ function renderSelector() {
918
+ const presets = getPresets();
919
+ if (presets.length === 0) { selector.style.display = 'none'; return; }
920
+ selector.style.display = 'inline-block';
921
+ selector.innerHTML = '<option value="">Presets</option>' +
922
+ presets.map((p, i) => `<option value="${i}">${window._escHtml(p.name)}</option>`).join('') +
923
+ '<option value="__clear">Clear all</option>';
924
+ }
925
+
926
+ saveBtn.addEventListener('click', () => {
927
+ const cli = document.querySelector('[data-cli-selector]');
928
+ const agent = document.querySelector('[data-agent-selector]');
929
+ const model = document.querySelector('[data-model-selector]');
930
+ const cliVal = cli?.value || '';
931
+ const agentVal = agent?.value || '';
932
+ const modelVal = model?.value || '';
933
+ if (!cliVal) return;
934
+ const parts = [cliVal.split('-').pop()];
935
+ if (agentVal) parts.push(agentVal.split('/').pop());
936
+ if (modelVal) parts.push(modelVal.split('/').pop().split('-')[0]);
937
+ const name = prompt('Preset name:', parts.join(' / '));
938
+ if (!name) return;
939
+ const presets = getPresets();
940
+ presets.push({ name, cli: cliVal, agent: agentVal, model: modelVal });
941
+ savePresets(presets);
942
+ renderSelector();
943
+ });
944
+
945
+ selector.addEventListener('change', () => {
946
+ const val = selector.value;
947
+ if (val === '__clear') { savePresets([]); renderSelector(); return; }
948
+ if (!val) return;
949
+ const presets = getPresets();
950
+ const preset = presets[parseInt(val)];
951
+ if (!preset) return;
952
+ const cli = document.querySelector('[data-cli-selector]');
953
+ const agent = document.querySelector('[data-agent-selector]');
954
+ const model = document.querySelector('[data-model-selector]');
955
+ if (cli && preset.cli) { cli.value = preset.cli; cli.dispatchEvent(new Event('change')); }
956
+ setTimeout(() => {
957
+ if (agent && preset.agent) { agent.value = preset.agent; agent.dispatchEvent(new Event('change')); }
958
+ setTimeout(() => {
959
+ if (model && preset.model) { model.value = preset.model; model.dispatchEvent(new Event('change')); }
960
+ }, 500);
961
+ }, 500);
962
+ selector.value = '';
963
+ });
964
+
965
+ renderSelector();
966
+ })();
967
+
906
968
  window.addEventListener('load', initializeApp);
@@ -275,6 +275,20 @@
275
275
  overflow: hidden;
276
276
  }
277
277
 
278
+ .conversation-item.pinned { cursor: grab; }
279
+ .conversation-item.pinned:active { cursor: grabbing; }
280
+ .conversation-item-checkbox {
281
+ flex-shrink: 0;
282
+ width: 16px;
283
+ height: 16px;
284
+ opacity: 0;
285
+ cursor: pointer;
286
+ margin: 0;
287
+ }
288
+ .conversation-item:hover .conversation-item-checkbox,
289
+ .conversation-item-checkbox:checked {
290
+ opacity: 1;
291
+ }
278
292
  .conversation-item-delete,
279
293
  .conversation-item-archive,
280
294
  .conversation-item-export {
@@ -3182,4 +3196,25 @@
3182
3196
  from { transform: translateX(100%); opacity: 0; }
3183
3197
  to { transform: translateX(0); opacity: 1; }
3184
3198
  }
3199
+
3200
+ .preset-btn {
3201
+ background: transparent;
3202
+ border: 1px solid var(--color-border);
3203
+ border-radius: 0.25rem;
3204
+ cursor: pointer;
3205
+ color: var(--color-text-secondary);
3206
+ font-size: 0.875rem;
3207
+ padding: 0.25rem 0.5rem;
3208
+ line-height: 1;
3209
+ }
3210
+ .preset-btn:hover { color: #f59e0b; }
3211
+ .preset-selector {
3212
+ font-size: 0.75rem;
3213
+ padding: 0.25rem;
3214
+ border: 1px solid var(--color-border);
3215
+ border-radius: 0.25rem;
3216
+ background: var(--color-bg);
3217
+ color: var(--color-text);
3218
+ max-width: 120px;
3219
+ }
3185
3220
 
package/static/index.html CHANGED
@@ -49,6 +49,11 @@
49
49
  <button class="clone-go-btn" id="cloneGoBtn" title="Clone">Go</button>
50
50
  <button class="clone-cancel-btn" id="cloneCancelBtn" title="Cancel">&times;</button>
51
51
  </div>
52
+ <div class="bulk-action-bar" id="bulkActionBar" style="display:none;padding:0.375rem 0.75rem;background:var(--color-primary);display:none;gap:0.5rem;align-items:center;">
53
+ <span id="bulkCount" style="color:white;font-size:0.75rem;flex:1;">0 selected</span>
54
+ <button id="bulkArchiveBtn" style="background:rgba(255,255,255,0.2);color:white;border:none;border-radius:0.25rem;padding:0.25rem 0.5rem;font-size:0.75rem;cursor:pointer;">Archive</button>
55
+ <button id="bulkDeleteBtn" style="background:rgba(255,255,255,0.2);color:white;border:none;border-radius:0.25rem;padding:0.25rem 0.5rem;font-size:0.75rem;cursor:pointer;">Delete</button>
56
+ </div>
52
57
  <ul class="sidebar-list" data-conversation-list role="listbox" aria-label="Conversations">
53
58
  <li class="sidebar-empty" data-conversation-empty>Loading...</li>
54
59
  </ul>
@@ -183,6 +188,10 @@
183
188
  <select class="agent-selector cli-selector" data-cli-selector title="Select CLI tool"></select>
184
189
  <select class="agent-selector sub-agent-selector" data-agent-selector title="Select sub-agent" style="display:none"></select>
185
190
  <select class="model-selector" data-model-selector title="Select model" data-empty="true"></select>
191
+ <div class="preset-controls" style="display:inline-flex;gap:0.25rem;margin-left:0.25rem;">
192
+ <select id="presetSelector" class="preset-selector" title="Load preset" style="display:none;"></select>
193
+ <button id="savePresetBtn" class="preset-btn" title="Save current agent/model as preset">&#9733;</button>
194
+ </div>
186
195
  <div class="message-input-container">
187
196
  <textarea
188
197
  class="message-textarea"
@@ -116,6 +116,48 @@ class ConversationManager {
116
116
  }
117
117
 
118
118
  setupDelegatedListeners() {
119
+ let draggedId = null;
120
+ this.listEl.addEventListener('dragstart', (e) => {
121
+ const item = e.target.closest('[data-drag-conv]');
122
+ if (!item) return;
123
+ draggedId = item.dataset.dragConv;
124
+ item.style.opacity = '0.5';
125
+ e.dataTransfer.effectAllowed = 'move';
126
+ });
127
+ this.listEl.addEventListener('dragend', (e) => {
128
+ const item = e.target.closest('[data-drag-conv]');
129
+ if (item) item.style.opacity = '';
130
+ draggedId = null;
131
+ });
132
+ this.listEl.addEventListener('dragover', (e) => {
133
+ const item = e.target.closest('[data-drag-conv]');
134
+ if (item && draggedId) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }
135
+ });
136
+ this.listEl.addEventListener('drop', (e) => {
137
+ e.preventDefault();
138
+ const target = e.target.closest('[data-drag-conv]');
139
+ if (!target || !draggedId || target.dataset.dragConv === draggedId) return;
140
+ const pinnedItems = [...this.listEl.querySelectorAll('[data-drag-conv]')];
141
+ const draggedEl = pinnedItems.find(el => el.dataset.dragConv === draggedId);
142
+ if (!draggedEl) return;
143
+ this.listEl.insertBefore(draggedEl, target);
144
+ const newOrder = [...this.listEl.querySelectorAll('[data-drag-conv]')].map(el => el.dataset.dragConv);
145
+ window.wsClient?.rpc('conv.reorder', { order: newOrder }).catch(() => {});
146
+ });
147
+
148
+ const bulkBar = document.getElementById('bulkActionBar');
149
+ const bulkCount = document.getElementById('bulkCount');
150
+ const updateBulkBar = () => {
151
+ const checked = this.getSelectedIds().length;
152
+ if (bulkBar) bulkBar.style.display = checked > 0 ? 'flex' : 'none';
153
+ if (bulkCount) bulkCount.textContent = `${checked} selected`;
154
+ };
155
+ this.listEl.addEventListener('change', (e) => {
156
+ if (e.target.matches('.conversation-item-checkbox')) updateBulkBar();
157
+ });
158
+ document.getElementById('bulkArchiveBtn')?.addEventListener('click', () => this.bulkArchive());
159
+ document.getElementById('bulkDeleteBtn')?.addEventListener('click', () => this.bulkDelete());
160
+
119
161
  this.listEl.addEventListener('click', (e) => {
120
162
  const exportBtn = e.target.closest('[data-export-conv]');
121
163
  if (exportBtn) {
@@ -484,11 +526,13 @@ class ConversationManager {
484
526
  const badge = isStreaming
485
527
  ? h('span', { class: 'conversation-streaming-badge', title: 'Streaming in progress' }, h('span', { class: 'streaming-dot' }))
486
528
  : null;
487
- return h('li', { class: 'conversation-item' + (isActive ? ' active' : ''), 'data-conv-id': conv.id },
529
+ const dragAttrs = conv.pinned ? { draggable: 'true', 'data-drag-conv': conv.id } : {};
530
+ return h('li', { class: 'conversation-item' + (isActive ? ' active' : '') + (conv.pinned ? ' pinned' : ''), 'data-conv-id': conv.id, ...dragAttrs },
488
531
  h('div', { class: 'conversation-item-content' },
489
532
  h('div', { class: 'conversation-item-title' }, ...(badge ? [badge, title] : [title])),
490
533
  h('div', { class: 'conversation-item-meta' }, metaParts.join(' \u2022 '))
491
534
  ),
535
+ h('input', { type: 'checkbox', class: 'conversation-item-checkbox', 'data-bulk-check': conv.id, title: 'Select for bulk action', onclick: 'event.stopPropagation()' }),
492
536
  h('button', { class: 'conversation-item-export', title: 'Export as markdown', 'data-export-conv': conv.id },
493
537
  h('svg', { width: '14', height: '14', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '2' },
494
538
  h('path', { d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }),
@@ -610,6 +654,30 @@ class ConversationManager {
610
654
  this.render();
611
655
  }
612
656
 
657
+ getSelectedIds() {
658
+ return [...this.listEl.querySelectorAll('.conversation-item-checkbox:checked')].map(cb => cb.dataset.bulkCheck);
659
+ }
660
+
661
+ async bulkArchive() {
662
+ const ids = this.getSelectedIds();
663
+ if (ids.length === 0) return;
664
+ if (!await window.UIDialog?.confirm(`Archive ${ids.length} conversation(s)?`, 'Bulk Archive')) return;
665
+ const base = window.__BASE_URL || '';
666
+ for (const id of ids) {
667
+ try { await fetch(`${base}/api/conversations/${id}/archive`, { method: 'POST' }); } catch (_) {}
668
+ this.deleteConversation(id);
669
+ }
670
+ }
671
+
672
+ async bulkDelete() {
673
+ const ids = this.getSelectedIds();
674
+ if (ids.length === 0) return;
675
+ if (!await window.UIDialog?.confirm(`Permanently delete ${ids.length} conversation(s)?`, 'Bulk Delete')) return;
676
+ for (const id of ids) {
677
+ this.confirmDelete(id, '');
678
+ }
679
+ }
680
+
613
681
  async exportConversation(convId) {
614
682
  try {
615
683
  const result = await window.wsClient.rpc('conv.export', { id: convId, format: 'markdown' });