@pingagent/sdk 0.1.11 → 0.1.13

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.
@@ -23,7 +23,7 @@ import {
23
23
  summarizeTrustPolicyAudit,
24
24
  updateStoredToken,
25
25
  upsertTrustPolicyRecommendation
26
- } from "./chunk-BLHMTUID.js";
26
+ } from "./chunk-N2GCIMAW.js";
27
27
 
28
28
  // src/web-server.ts
29
29
  import * as fs from "fs";
@@ -153,6 +153,20 @@ function getHostPanelHtml() {
153
153
  }
154
154
  .empty { color: #94a3b8; font-size: 13px; }
155
155
  .link-row { display: flex; gap: 10px; margin-top: 20px; }
156
+ .toolbar-row { display: flex; justify-content: space-between; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
157
+ .toolbar-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
158
+ .mode-toggle { display: inline-flex; gap: 6px; padding: 4px; border-radius: 999px; border: 1px solid #334155; background: #020617; }
159
+ .mode-toggle button {
160
+ border: 0;
161
+ background: transparent;
162
+ color: #94a3b8;
163
+ border-radius: 999px;
164
+ padding: 6px 10px;
165
+ cursor: pointer;
166
+ }
167
+ .mode-toggle button.active { background: #0f766e; color: #ecfeff; }
168
+ .summary-pills { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
169
+ .summary-pills .pill { font-size: 11px; }
156
170
  @media (max-width: 1000px) {
157
171
  .layout { grid-template-columns: 1fr; }
158
172
  .sidebar { border-right: none; border-bottom: 1px solid #1e293b; }
@@ -196,7 +210,17 @@ function getHostPanelHtml() {
196
210
  <div class="grid stats" id="statsGrid"></div>
197
211
  <div class="grid runtime-layout" style="margin-top:16px">
198
212
  <div class="card">
199
- <h2>Recent Sessions</h2>
213
+ <div class="toolbar-row">
214
+ <h2 style="margin:0">Recent Sessions</h2>
215
+ <div class="toolbar-actions">
216
+ <button class="secondary-btn" id="toggleUnreadBtn" style="width:auto">Unread only: off</button>
217
+ <button class="secondary-btn" id="nextUnreadBtn" style="width:auto">Next unread</button>
218
+ <div class="mode-toggle" aria-label="Runtime detail mode">
219
+ <button id="detailModeBasicBtn" class="active" type="button">Basic</button>
220
+ <button id="detailModeAdvancedBtn" type="button">Advanced</button>
221
+ </div>
222
+ </div>
223
+ </div>
200
224
  <div class="sessions" id="sessionList"></div>
201
225
  </div>
202
226
  <div class="grid">
@@ -325,14 +349,28 @@ function getHostPanelHtml() {
325
349
  </div>
326
350
 
327
351
  <script>
352
+ const initialQuery = (function () {
353
+ const params = new URLSearchParams(window.location.search);
354
+ const profile = params.get('profile');
355
+ const sessionKey = params.get('session_key');
356
+ const view = params.get('view');
357
+ return {
358
+ profile: profile && profile.trim() ? profile.trim() : null,
359
+ sessionKey: sessionKey && sessionKey.trim() ? sessionKey.trim() : null,
360
+ view: view === 'policy' ? 'policy' : 'runtime',
361
+ };
362
+ })();
363
+
328
364
  const state = {
329
- selectedProfile: sessionStorage.getItem('pingagent_host_panel_profile') || null,
330
- currentTab: 'runtime',
365
+ selectedProfile: initialQuery.profile || sessionStorage.getItem('pingagent_host_panel_profile') || null,
366
+ currentTab: initialQuery.view,
331
367
  profiles: [],
332
368
  overview: null,
333
369
  session: null,
334
370
  policy: null,
335
- selectedSessionKey: null,
371
+ selectedSessionKey: initialQuery.sessionKey || null,
372
+ detailMode: sessionStorage.getItem('pingagent_host_panel_detail_mode') || 'basic',
373
+ showUnreadOnly: false,
336
374
  };
337
375
 
338
376
  function esc(value) {
@@ -356,6 +394,45 @@ function getHostPanelHtml() {
356
394
  try { return new Date(value).toLocaleString(); } catch { return String(value); }
357
395
  }
358
396
 
397
+ function syncUrlState() {
398
+ const url = new URL(window.location.href);
399
+ if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
400
+ else url.searchParams.delete('profile');
401
+ if (state.selectedSessionKey) url.searchParams.set('session_key', state.selectedSessionKey);
402
+ else url.searchParams.delete('session_key');
403
+ url.searchParams.set('view', state.currentTab === 'policy' ? 'policy' : 'runtime');
404
+ history.replaceState(null, '', url.pathname + (url.search ? url.search : ''));
405
+ }
406
+
407
+ function setDetailMode(mode) {
408
+ state.detailMode = mode === 'advanced' ? 'advanced' : 'basic';
409
+ sessionStorage.setItem('pingagent_host_panel_detail_mode', state.detailMode);
410
+ document.getElementById('detailModeBasicBtn').classList.toggle('active', state.detailMode === 'basic');
411
+ document.getElementById('detailModeAdvancedBtn').classList.toggle('active', state.detailMode === 'advanced');
412
+ if (state.overview) renderOverview();
413
+ if (state.session) renderSession();
414
+ }
415
+
416
+ function buildSessionLink(sessionKey) {
417
+ const url = new URL(window.location.href);
418
+ if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
419
+ else url.searchParams.delete('profile');
420
+ if (sessionKey) url.searchParams.set('session_key', sessionKey);
421
+ else url.searchParams.delete('session_key');
422
+ url.searchParams.set('view', 'runtime');
423
+ return url.toString();
424
+ }
425
+
426
+ async function copyText(text, fallbackLabel) {
427
+ try {
428
+ await navigator.clipboard.writeText(text);
429
+ window.alert((fallbackLabel || 'Copied') + ':
430
+ ' + text);
431
+ } catch {
432
+ window.prompt(fallbackLabel || 'Copy', text);
433
+ }
434
+ }
435
+
359
436
  async function api(path, opts) {
360
437
  let url = path;
361
438
  if (state.selectedProfile) {
@@ -405,6 +482,7 @@ function getHostPanelHtml() {
405
482
  document.getElementById('navPolicy').classList.toggle('active', tab === 'policy');
406
483
  document.getElementById('runtimePanel').classList.toggle('active', tab === 'runtime');
407
484
  document.getElementById('policyPanel').classList.toggle('active', tab === 'policy');
485
+ syncUrlState();
408
486
  }
409
487
 
410
488
  function renderHeader() {
@@ -494,9 +572,40 @@ function getHostPanelHtml() {
494
572
  '</div>';
495
573
  }
496
574
 
575
+ function getOverviewSessions() {
576
+ return state.overview && Array.isArray(state.overview.sessions) ? state.overview.sessions : [];
577
+ }
578
+
579
+ function getVisibleSessions() {
580
+ const sessions = getOverviewSessions();
581
+ return state.showUnreadOnly
582
+ ? sessions.filter(function (session) { return Number(session.unread_count || 0) > 0; })
583
+ : sessions;
584
+ }
585
+
586
+ function syncSelectedSessionFromOverview() {
587
+ const allSessions = getOverviewSessions();
588
+ const visibleSessions = getVisibleSessions();
589
+ if (!allSessions.length) {
590
+ state.selectedSessionKey = null;
591
+ state.session = null;
592
+ return;
593
+ }
594
+ const hasSelected = allSessions.some(function (session) { return session.session_key === state.selectedSessionKey; });
595
+ if (!hasSelected) {
596
+ state.selectedSessionKey = visibleSessions.length ? visibleSessions[0].session_key : allSessions[0].session_key;
597
+ return;
598
+ }
599
+ if (state.showUnreadOnly) {
600
+ const stillVisible = visibleSessions.some(function (session) { return session.session_key === state.selectedSessionKey; });
601
+ if (!stillVisible) state.selectedSessionKey = visibleSessions.length ? visibleSessions[0].session_key : null;
602
+ }
603
+ }
604
+
497
605
  function renderOverview() {
498
606
  const overview = state.overview;
499
607
  if (!overview) return;
608
+ syncSelectedSessionFromOverview();
500
609
  const ingressState = ingressStatusModel(overview);
501
610
  document.getElementById('activationCard').innerHTML =
502
611
  '<div class="status-strip">' +
@@ -536,24 +645,27 @@ function getHostPanelHtml() {
536
645
  return '<div class="card"><div class="label">' + esc(item.label) + '</div><div class="value">' + esc(item.value) + '</div><div class="muted small">' + esc(item.sub) + '</div></div>';
537
646
  }).join('');
538
647
 
539
- const sessions = Array.isArray(overview.sessions) ? overview.sessions : [];
648
+ const toggleUnreadBtn = document.getElementById('toggleUnreadBtn');
649
+ if (toggleUnreadBtn) toggleUnreadBtn.textContent = 'Unread only: ' + (state.showUnreadOnly ? 'on' : 'off');
650
+ const sessions = getVisibleSessions();
540
651
  if (!sessions.length) {
541
- document.getElementById('sessionList').innerHTML = '<div class="empty">No sessions yet.</div>';
652
+ document.getElementById('sessionList').innerHTML = '<div class="empty">' + (state.showUnreadOnly ? 'No unread sessions.' : 'No sessions yet.') + '</div>';
542
653
  } else {
543
- if (!state.selectedSessionKey) state.selectedSessionKey = sessions[0].session_key;
544
654
  document.getElementById('sessionList').innerHTML = sessions.map(function (session) {
545
655
  const active = session.session_key === state.selectedSessionKey ? ' active' : '';
546
656
  const badges = [
547
657
  '<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>',
548
658
  session.binding_alert
549
- ? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="' + esc(session.binding_alert.message || 'Rebind this conversation to the current chat session') + '">Needs rebind</button>'
659
+ ? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="OpenClaw chat link needs attention">Needs reconnect</button>'
550
660
  : '',
551
661
  ].filter(Boolean).join('');
552
662
  return '<div class="session-row' + active + '" data-session="' + esc(session.session_key) + '">' +
553
- '<div class="top"><strong>' + esc(session.remote_did || session.conversation_id || 'unknown') + '</strong><div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">' + badges + '</div></div>' +
554
- '<div class="muted small" style="margin-top:6px">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
663
+ '<div class="top"><strong>' + esc(session.remote_did || session.session_key || 'unknown') + '</strong><div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">' + badges + '</div></div>' +
555
664
  '<div class="muted small">unread=' + esc(session.unread_count) + ' \xB7 last=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
556
- '<div class="muted small">work_session=' + esc(session.mapped_work_session || '(unbound)') + (session.is_active_work_session ? ' \xB7 active_chat=true' : '') + '</div>' +
665
+ (state.detailMode === 'advanced'
666
+ ? '<div class="muted small" style="margin-top:6px">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
667
+ '<div class="muted small">work_session=' + esc(session.mapped_work_session || '(unbound)') + (session.is_active_work_session ? ' \xB7 active_chat=true' : '') + '</div>'
668
+ : '') +
557
669
  '<div class="muted small" style="margin-top:6px">' + esc(session.last_message_preview || '(no preview)') + '</div>' +
558
670
  '</div>';
559
671
  }).join('');
@@ -678,25 +790,47 @@ conversation=' + result.conversation_id));
678
790
  const bindingAlert = detail.bindingAlert || null;
679
791
  const activeWorkSession = detail.activeWorkSession || null;
680
792
  const summary = detail.sessionSummary || null;
793
+ const isAdvanced = state.detailMode === 'advanced';
794
+ const sessionLink = buildSessionLink(session.session_key);
795
+ const summaryPills = [];
796
+ if (contact && contact.action) summaryPills.push('<span class="pill">contact=' + esc(contact.action) + '</span>');
797
+ if (task && task.action) summaryPills.push('<span class="pill">task=' + esc(task.action) + '</span>');
798
+ if (session.trust_state) summaryPills.push('<span class="pill">trust=' + esc(session.trust_state) + '</span>');
799
+ if (Number(session.unread_count || 0) > 0) summaryPills.push('<span class="pill">unread=' + esc(session.unread_count) + '</span>');
800
+ if (binding && binding.session_key) summaryPills.push('<span class="pill">chat link attached</span>');
801
+ else if (activeWorkSession) summaryPills.push('<span class="pill">current OpenClaw chat available</span>');
802
+ const policyBlock = isAdvanced
803
+ ? '<pre style="margin-top:8px">[Contact]\\naction=' + esc(contact.action) + '\\nsource=' + esc(contact.source) + (contact.matched_rule ? '\\nmatched_rule=' + esc(contact.matched_rule) : '') + '\\n' + esc(contact.explanation) + '\\n\\n[Task]\\naction=' + esc(task.action) + '\\nsource=' + esc(task.source) + (task.matched_rule ? '\\nmatched_rule=' + esc(task.matched_rule) : '') + '\\n' + esc(task.explanation) + '</pre>'
804
+ : '<div class="summary-pills" style="margin-top:8px">' + summaryPills.join('') + '</div>' +
805
+ '<div class="muted small" style="margin-top:10px">' + esc(contact.explanation || '(no contact explanation)') + '</div>' +
806
+ '<div class="muted small" style="margin-top:6px">' + esc(task.explanation || '(no task explanation)') + '</div>';
681
807
 
682
808
  el.innerHTML = '' +
683
809
  '<div class="two-col">' +
684
810
  '<div>' +
685
811
  '<div class="label">Session</div>' +
686
812
  '<div style="margin-top:8px"><strong>' + esc(session.remote_did || '(unknown)') + '</strong></div>' +
687
- '<div class="muted small">session=' + esc(session.session_key) + '</div>' +
688
- '<div class="muted small">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
689
813
  '<div class="muted small">trust=' + esc(session.trust_state) + ' \xB7 unread=' + esc(session.unread_count) + '</div>' +
690
814
  '<div class="muted small">last activity=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
691
- '<div class="muted small" style="margin-top:8px">active_chat_session=' + esc(activeWorkSession || '(none)') + '</div>' +
692
- '<div class="muted small">binding=' + esc(binding ? binding.session_key : '(unbound)') + '</div>' +
815
+ (isAdvanced
816
+ ? '<div style="margin-top:8px">' +
817
+ '<div class="muted small">session=' + esc(session.session_key) + '</div>' +
818
+ '<div class="muted small">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
819
+ '<div class="muted small">active_chat_session=' + esc(activeWorkSession || '(none)') + '</div>' +
820
+ '<div class="muted small">binding=' + esc(binding ? binding.session_key : '(unbound)') + '</div>' +
821
+ '</div>'
822
+ : '') +
693
823
  (openRecommendation
694
824
  ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(openRecommendation)) + '</div>'
695
825
  : (reopenRecommendation ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(reopenRecommendation)) + '</div>' : '')) +
826
+ (summaryPills.length ? '<div class="summary-pills">' + summaryPills.join('') + '</div>' : '') +
696
827
  (bindingAlert
697
- ? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs rebind</strong><div class="small" style="margin-top:6px">' + esc(bindingAlert.message || 'Bound work session is missing. Rebind this PingAgent conversation to the current chat session.') + '</div></div>'
828
+ ? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs reconnect</strong><div class="small" style="margin-top:6px">' + esc(isAdvanced ? (bindingAlert.message || 'Bound work session is missing. Rebind this PingAgent conversation to the current chat session.') : 'OpenClaw chat link is stale. Attach this PingAgent session to the current OpenClaw chat.') + '</div></div>'
698
829
  : '') +
699
830
  '<div class="row-actions">' +
831
+ (session.trust_state === 'pending'
832
+ ? '<button class="action-btn approve-session-btn" data-session="' + esc(session.session_key) + '">Approve Contact</button>'
833
+ : '') +
700
834
  (openRecommendation
701
835
  ? '<button class="action-btn apply-session-recommendation-btn" data-session="' + esc(session.session_key) + '">' + esc(recommendationActionLabel(openRecommendation)) + '</button>'
702
836
  : '') +
@@ -706,13 +840,24 @@ conversation=' + result.conversation_id));
706
840
  (!openRecommendation && reopenRecommendation
707
841
  ? '<button class="secondary-btn reopen-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Reopen</button>'
708
842
  : '') +
709
- '<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Bind Current Chat</button>' +
710
- '<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Clear Binding</button>' +
843
+ '<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Attach to Current Chat</button>' +
844
+ '<button class="secondary-btn mark-read-btn" data-session="' + esc(session.session_key) + '">Mark read</button>' +
845
+ '<button class="secondary-btn copy-session-link-btn" data-session="' + esc(session.session_key) + '">Copy Session Link</button>' +
846
+ '<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Detach Chat Link</button>' +
847
+ '</div>' +
848
+ '<div class="form-grid" style="margin-top:16px">' +
849
+ '<label class="label">Reply in this session</label>' +
850
+ '<textarea id="sessionReplyInput" placeholder="Send a text reply in this session"></textarea>' +
851
+ '<div class="row-actions"><button class="action-btn" id="sendSessionReplyBtn">Send Reply</button></div>' +
711
852
  '</div>' +
712
853
  '</div>' +
713
854
  '<div>' +
714
855
  '<div class="label">Policy Decisions</div>' +
715
- '<pre style="margin-top:8px">[Contact]\\naction=' + esc(contact.action) + '\\nsource=' + esc(contact.source) + (contact.matched_rule ? '\\nmatched_rule=' + esc(contact.matched_rule) : '') + '\\n' + esc(contact.explanation) + '\\n\\n[Task]\\naction=' + esc(task.action) + '\\nsource=' + esc(task.source) + (task.matched_rule ? '\\nmatched_rule=' + esc(task.matched_rule) : '') + '\\n' + esc(task.explanation) + '</pre>' +
856
+ policyBlock +
857
+ (isAdvanced && recommendations.length
858
+ ? '<pre style="margin-top:12px">recommendations_debug=' + esc(JSON.stringify(recommendations.map(function (item) { return { id: item.id, status: item.status, policy: item.policy, action: item.action, current_action: item.current_action, match: item.match, confidence: item.confidence }; }), null, 2)) + '</pre>'
859
+ : '') +
860
+ (isAdvanced ? '<div class="muted small" style="margin-top:10px">permalink=' + esc(sessionLink) + '</div>' : '') +
716
861
  '</div>' +
717
862
  '</div>' +
718
863
  '<div class="grid two-col" style="margin-top:16px">' +
@@ -730,9 +875,7 @@ conversation=' + result.conversation_id));
730
875
  '<textarea id="sessionSummaryOpenQuestions" placeholder="Open questions">' + esc(summary && summary.open_questions ? summary.open_questions : '') + '</textarea>' +
731
876
  '<textarea id="sessionSummaryNextAction" placeholder="Next action">' + esc(summary && summary.next_action ? summary.next_action : '') + '</textarea>' +
732
877
  '<textarea id="sessionSummaryHandoff" placeholder="Handoff-ready summary">' + esc(summary && summary.handoff_ready_text ? summary.handoff_ready_text : '') + '</textarea>' +
733
- '<div class="row-actions">' +
734
- '<button class="action-btn" id="saveSessionSummaryBtn">Save Summary</button>' +
735
- '</div>' +
878
+ '<div class="row-actions"><button class="action-btn" id="saveSessionSummaryBtn">Save Summary</button></div>' +
736
879
  '</div>' +
737
880
  '</div>' +
738
881
  '</div>' +
@@ -759,8 +902,8 @@ conversation=' + result.conversation_id));
759
902
  ? '<button class="secondary-btn reopen-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Reopen</button>'
760
903
  : '';
761
904
  return '<div class="recommendation-row"><div class="top"><strong>' + esc(item.policy) + '</strong><span class="badge">' + esc(item.status + ' \xB7 ' + item.action) + '</span></div>' +
762
- '<div class="muted small">current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' +
763
- '<div class="muted small">match=' + esc(item.match) + '</div>' +
905
+ (isAdvanced ? '<div class="muted small">current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' : '') +
906
+ (isAdvanced ? '<div class="muted small">match=' + esc(item.match) + '</div>' : '') +
764
907
  '<div style="margin-top:8px">' + esc(item.reason) + '</div>' +
765
908
  '<div class="row-actions">' + actionButton + dismissButton + reopenButton + '</div>' +
766
909
  '</div>';
@@ -770,10 +913,10 @@ conversation=' + result.conversation_id));
770
913
  '<div class="grid two-col" style="margin-top:16px">' +
771
914
  '<div><div class="label">Recent Messages</div><div class="message-list" style="margin-top:8px">' +
772
915
  (messages.length ? messages.map(function (msg) {
773
- const summary = msg.schema === 'pingagent.text@1' && msg.payload && msg.payload.text
916
+ const messageSummary = msg.schema === 'pingagent.text@1' && msg.payload && msg.payload.text
774
917
  ? msg.payload.text
775
918
  : JSON.stringify(msg.payload || {});
776
- return '<div class="message-row"><div class="muted small">' + esc(fmtTs(msg.ts_ms)) + ' \xB7 ' + esc(msg.direction) + ' \xB7 ' + esc(msg.schema) + '</div><div style="margin-top:8px">' + esc(summary) + '</div></div>';
919
+ return '<div class="message-row"><div class="muted small">' + esc(fmtTs(msg.ts_ms)) + ' \xB7 ' + esc(msg.direction) + ' \xB7 ' + esc(msg.schema) + '</div><div style="margin-top:8px">' + esc(messageSummary) + '</div></div>';
777
920
  }).join('') : '<div class="empty">No local message history yet.</div>') +
778
921
  '</div></div>' +
779
922
  '<div><div class="label">Policy Audit</div><div class="audit-list" style="margin-top:8px">' +
@@ -786,6 +929,24 @@ conversation=' + result.conversation_id));
786
929
  '</div></div>' +
787
930
  '</div>';
788
931
 
932
+ el.querySelectorAll('.approve-session-btn').forEach(function (btn) {
933
+ btn.addEventListener('click', async function () {
934
+ const sessionKey = btn.getAttribute('data-session');
935
+ const result = await api('/api/runtime/session/approve', {
936
+ method: 'POST',
937
+ headers: { 'Content-Type': 'application/json' },
938
+ body: JSON.stringify({ session_key: sessionKey }),
939
+ });
940
+ await refreshAll();
941
+ const promoted = result && result.dm_conversation_id
942
+ ? getOverviewSessions().find(function (item) { return item.conversation_id === result.dm_conversation_id; })
943
+ : null;
944
+ state.selectedSessionKey = promoted ? promoted.session_key : sessionKey;
945
+ renderOverview();
946
+ if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
947
+ setTab('runtime');
948
+ });
949
+ });
789
950
  el.querySelectorAll('.bind-current-btn').forEach(function (btn) {
790
951
  btn.addEventListener('click', async function () {
791
952
  const conversationId = btn.getAttribute('data-conversation');
@@ -793,6 +954,22 @@ conversation=' + result.conversation_id));
793
954
  await promptBindCurrentChat(conversationId);
794
955
  });
795
956
  });
957
+ el.querySelectorAll('.mark-read-btn').forEach(function (btn) {
958
+ btn.addEventListener('click', async function () {
959
+ await api('/api/runtime/session/mark-read', {
960
+ method: 'POST',
961
+ headers: { 'Content-Type': 'application/json' },
962
+ body: JSON.stringify({ session_key: btn.getAttribute('data-session') }),
963
+ });
964
+ await refreshAll();
965
+ setTab('runtime');
966
+ });
967
+ });
968
+ el.querySelectorAll('.copy-session-link-btn').forEach(function (btn) {
969
+ btn.addEventListener('click', async function () {
970
+ await copyText(buildSessionLink(btn.getAttribute('data-session')), 'Session link');
971
+ });
972
+ });
796
973
  el.querySelectorAll('.clear-binding-btn').forEach(function (btn) {
797
974
  btn.addEventListener('click', async function () {
798
975
  await api('/api/runtime/session-bindings/clear', {
@@ -837,23 +1014,42 @@ conversation=' + result.conversation_id));
837
1014
  setTab('runtime');
838
1015
  });
839
1016
  });
1017
+ const sendSessionReplyBtn = document.getElementById('sendSessionReplyBtn');
1018
+ if (sendSessionReplyBtn) {
1019
+ sendSessionReplyBtn.addEventListener('click', async function () {
1020
+ const input = document.getElementById('sessionReplyInput');
1021
+ const message = input.value.trim();
1022
+ if (!message) {
1023
+ window.alert('Reply text is required.');
1024
+ return;
1025
+ }
1026
+ await api('/api/runtime/session/reply', {
1027
+ method: 'POST',
1028
+ headers: { 'Content-Type': 'application/json' },
1029
+ body: JSON.stringify({ session_key: session.session_key, message: message }),
1030
+ });
1031
+ input.value = '';
1032
+ await refreshAll();
1033
+ setTab('runtime');
1034
+ });
1035
+ }
840
1036
  const saveSessionSummaryBtn = document.getElementById('saveSessionSummaryBtn');
841
1037
  if (saveSessionSummaryBtn) {
842
1038
  saveSessionSummaryBtn.addEventListener('click', async function () {
843
1039
  await api('/api/runtime/session-summary', {
844
1040
  method: 'POST',
845
1041
  headers: { 'Content-Type': 'application/json' },
846
- body: JSON.stringify({
847
- session_key: session.session_key,
848
- objective: document.getElementById('sessionSummaryObjective').value.trim(),
849
- context: document.getElementById('sessionSummaryContext').value.trim(),
850
- constraints: document.getElementById('sessionSummaryConstraints').value.trim(),
851
- decisions: document.getElementById('sessionSummaryDecisions').value.trim(),
852
- open_questions: document.getElementById('sessionSummaryOpenQuestions').value.trim(),
853
- next_action: document.getElementById('sessionSummaryNextAction').value.trim(),
854
- handoff_ready_text: document.getElementById('sessionSummaryHandoff').value.trim(),
855
- }),
856
- });
1042
+ body: JSON.stringify({
1043
+ session_key: session.session_key,
1044
+ objective: document.getElementById('sessionSummaryObjective').value.trim(),
1045
+ context: document.getElementById('sessionSummaryContext').value.trim(),
1046
+ constraints: document.getElementById('sessionSummaryConstraints').value.trim(),
1047
+ decisions: document.getElementById('sessionSummaryDecisions').value.trim(),
1048
+ open_questions: document.getElementById('sessionSummaryOpenQuestions').value.trim(),
1049
+ next_action: document.getElementById('sessionSummaryNextAction').value.trim(),
1050
+ handoff_ready_text: document.getElementById('sessionSummaryHandoff').value.trim(),
1051
+ }),
1052
+ });
857
1053
  await refreshAll();
858
1054
  setTab('runtime');
859
1055
  });
@@ -867,7 +1063,7 @@ conversation=' + result.conversation_id));
867
1063
  const previous = previousBinding || (state.session && state.session.binding ? state.session.binding.session_key : null) || '(unbound)';
868
1064
  const targetRemoteDid = remoteDid || (state.session && state.session.session ? state.session.session.remote_did : null) || '(unknown)';
869
1065
  const confirmed = window.confirm(
870
- 'Rebind this PingAgent conversation to the current chat session?' +
1066
+ 'Attach this PingAgent session to the current OpenClaw chat?' +
871
1067
  '
872
1068
 
873
1069
  Conversation: ' + conversationId +
@@ -875,9 +1071,9 @@ Conversation: ' + conversationId +
875
1071
  Remote DID: ' + targetRemoteDid +
876
1072
  '
877
1073
 
878
- Current chat: ' + (current || '(none)') +
1074
+ Current OpenClaw chat: ' + (current || '(none)') +
879
1075
  '
880
- Previous binding: ' + previous
1076
+ Previous chat link: ' + previous
881
1077
  );
882
1078
  if (!confirmed) return;
883
1079
  await api('/api/runtime/session-bindings/bind-current', {
@@ -1061,14 +1257,15 @@ Previous binding: ' + previous
1061
1257
 
1062
1258
  async function loadOverview() {
1063
1259
  state.overview = await api('/api/runtime/overview');
1260
+ syncSelectedSessionFromOverview();
1064
1261
  renderHeader();
1065
1262
  renderOverview();
1066
- const sessions = state.overview && Array.isArray(state.overview.sessions) ? state.overview.sessions : [];
1067
- if (!state.selectedSessionKey && sessions.length) {
1068
- state.selectedSessionKey = sessions[0].session_key;
1069
- }
1070
1263
  if (state.selectedSessionKey) {
1071
1264
  await loadSession(state.selectedSessionKey);
1265
+ } else {
1266
+ state.session = null;
1267
+ renderSession();
1268
+ syncUrlState();
1072
1269
  }
1073
1270
  }
1074
1271
 
@@ -1076,6 +1273,7 @@ Previous binding: ' + previous
1076
1273
  if (!sessionKey) return;
1077
1274
  state.selectedSessionKey = sessionKey;
1078
1275
  state.session = await api('/api/runtime/session?session_key=' + encodeURIComponent(sessionKey));
1276
+ syncUrlState();
1079
1277
  renderSession();
1080
1278
  }
1081
1279
 
@@ -1096,6 +1294,30 @@ Previous binding: ' + previous
1096
1294
 
1097
1295
  document.getElementById('navRuntime').addEventListener('click', function () { setTab('runtime'); });
1098
1296
  document.getElementById('navPolicy').addEventListener('click', function () { setTab('policy'); });
1297
+ document.getElementById('toggleUnreadBtn').addEventListener('click', async function () {
1298
+ state.showUnreadOnly = !state.showUnreadOnly;
1299
+ syncSelectedSessionFromOverview();
1300
+ renderOverview();
1301
+ if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
1302
+ else {
1303
+ syncUrlState();
1304
+ renderSession();
1305
+ }
1306
+ });
1307
+ document.getElementById('nextUnreadBtn').addEventListener('click', async function () {
1308
+ const unreadSessions = getOverviewSessions().filter(function (session) { return Number(session.unread_count || 0) > 0; });
1309
+ if (!unreadSessions.length) {
1310
+ window.alert('No unread sessions.');
1311
+ return;
1312
+ }
1313
+ const currentIndex = unreadSessions.findIndex(function (session) { return session.session_key === state.selectedSessionKey; });
1314
+ const next = unreadSessions[(currentIndex + 1 + unreadSessions.length) % unreadSessions.length];
1315
+ state.selectedSessionKey = next.session_key;
1316
+ renderOverview();
1317
+ await loadSession(state.selectedSessionKey);
1318
+ });
1319
+ document.getElementById('detailModeBasicBtn').addEventListener('click', function () { setDetailMode('basic'); });
1320
+ document.getElementById('detailModeAdvancedBtn').addEventListener('click', function () { setDetailMode('advanced'); });
1099
1321
  document.getElementById('rulePolicy').addEventListener('change', updateRuleActionOptions);
1100
1322
  document.getElementById('saveDefaultsBtn').addEventListener('click', async function () {
1101
1323
  await api('/api/runtime/policy/defaults', {
@@ -1178,9 +1400,12 @@ Previous binding: ' + previous
1178
1400
  });
1179
1401
 
1180
1402
  async function init() {
1403
+ setTab(state.currentTab);
1404
+ setDetailMode(state.detailMode);
1181
1405
  await loadProfiles();
1182
1406
  updateRuleActionOptions();
1183
1407
  await refreshAll();
1408
+ syncUrlState();
1184
1409
  }
1185
1410
 
1186
1411
  init().catch(function (error) {
@@ -1679,6 +1904,18 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
1679
1904
  }))
1680
1905
  };
1681
1906
  }
1907
+ function resolveSessionForInput(sessionManager, input) {
1908
+ if (!sessionManager) return null;
1909
+ const sessionKey = String(input.session_key ?? "").trim();
1910
+ const conversationId = String(input.conversation_id ?? "").trim();
1911
+ const remoteDid = String(input.remote_did ?? "").trim();
1912
+ let session = sessionKey ? sessionManager.get(sessionKey) : null;
1913
+ if (!session && conversationId) session = sessionManager.getByConversationId(conversationId);
1914
+ if (!session && remoteDid) {
1915
+ session = sessionManager.listRecentSessions(100).find((item) => item.remote_did === remoteDid) ?? null;
1916
+ }
1917
+ return session ?? sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
1918
+ }
1682
1919
  async function handleApi(pathname, req, ctx) {
1683
1920
  const client = ctx.client;
1684
1921
  const contactManager = ctx.contactManager;
@@ -1780,6 +2017,48 @@ async function handleApi(pathname, req, ctx) {
1780
2017
  };
1781
2018
  }
1782
2019
  if (parts[1] === "session") {
2020
+ const sessionManager = ctx.client.getSessionManager();
2021
+ if (!sessionManager) throw new Error("Session actions require a writable local store");
2022
+ if (parts[2] === "reply" && req.method === "POST") {
2023
+ const body = await readBody(req);
2024
+ const session = resolveSessionForInput(sessionManager, body);
2025
+ if (!session?.conversation_id) throw new Error("No session selected");
2026
+ const text = String(body?.message ?? "").trim();
2027
+ if (!text) throw new Error("Missing message");
2028
+ const sendRes = await client.sendMessage(session.conversation_id, SCHEMA_TEXT, { text });
2029
+ if (!sendRes.ok) throw new Error(sendRes.error?.message ?? "Failed to send");
2030
+ sessionManager.focusSession(session.session_key);
2031
+ return {
2032
+ ok: true,
2033
+ session: sessionManager.get(session.session_key),
2034
+ message_id: sendRes.data?.message_id ?? null
2035
+ };
2036
+ }
2037
+ if (parts[2] === "approve" && req.method === "POST") {
2038
+ const body = await readBody(req);
2039
+ const session = resolveSessionForInput(sessionManager, body);
2040
+ if (!session?.conversation_id) throw new Error("No session selected");
2041
+ const approveRes = await client.approveContact(session.conversation_id);
2042
+ if (!approveRes.ok) throw new Error(approveRes.error?.message ?? "Failed to approve contact");
2043
+ sessionManager.focusSession(session.session_key);
2044
+ return {
2045
+ ok: true,
2046
+ session: sessionManager.get(session.session_key),
2047
+ trusted: approveRes.data?.trusted ?? true,
2048
+ dm_conversation_id: approveRes.data?.dm_conversation_id ?? session.conversation_id
2049
+ };
2050
+ }
2051
+ if (parts[2] === "mark-read" && req.method === "POST") {
2052
+ const body = await readBody(req);
2053
+ const session = resolveSessionForInput(sessionManager, body);
2054
+ if (!session?.session_key) throw new Error("No session selected");
2055
+ const updated = sessionManager.markRead(session.session_key);
2056
+ if (!updated) throw new Error("Failed to mark session as read");
2057
+ return {
2058
+ ok: true,
2059
+ session: updated
2060
+ };
2061
+ }
1783
2062
  const url = new URL(req.url || "", "http://x");
1784
2063
  const sessionKey = url.searchParams.get("session_key");
1785
2064
  return buildSessionOverviewPayload(ctx, sessionKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pingagent/sdk",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,7 +35,7 @@
35
35
  "uuid": "^11.0.0",
36
36
  "ws": "^8.0.0",
37
37
  "@pingagent/protocol": "0.1.1",
38
- "@pingagent/schemas": "0.1.2",
38
+ "@pingagent/schemas": "0.1.3",
39
39
  "@pingagent/a2a": "0.1.1"
40
40
  },
41
41
  "devDependencies": {