roboticus-api 0.11.4

HTTP routes, WebSocket, auth, rate limiting, and dashboard for the Roboticus agent runtime
Documentation
App._activeSession = null;
App._sessionMessages = [];

App.renderSessions = function() {
      var self = this;
      return Promise.all([
        api('/api/sessions'),
        api('/api/agent/status').catch(function() { return {}; })
      ]).then(function(results) {
        var data = results[0] || {};
        var agentStatus = results[1] || {};
        if (agentStatus.name) setAgentDisplayName(agentStatus.name);
        var rootAgentId = (agentStatus.agent_id || agentStatus.id || '').toLowerCase();
        var sessions = data.sessions || [];
        if (sessions.length > 0) {
          dismissHint('sessions-helper');
          try { window.localStorage.setItem('ic_sessions_helper_dismissed', '1'); } catch (_) {}
        }

        if (self._activeSession) {
          return Promise.all([
            api('/api/sessions/' + encodeURIComponent(self._activeSession.id) + '/messages').catch(function() { return { messages: [] }; }),
            api('/api/sessions/' + encodeURIComponent(self._activeSession.id) + '/turns').catch(function() { return { turns: [] }; }),
            api('/api/sessions/' + encodeURIComponent(self._activeSession.id) + '/feedback').catch(function() { return { feedback: [] }; })
          ]).then(function(results) {
            var r = results[0], turnsData = results[1], fbData = results[2];
            self._sessionMessages = r.messages || [];
            var turns = turnsData.turns || [];
            var feedbackByTurn = {};
            (fbData.feedback || []).forEach(function(fb) { feedbackByTurn[fb.turn_id] = fb; });
            var asstCount = 0;
            var msgs = self._sessionMessages.map(function(m, idx) {
              var side = m.role === 'user' ? 'user' : 'assistant';
              var roleLabel = side === 'assistant'
                ? sessionAssistantLabel(self._activeSession, m.role || 'assistant')
                : (m.role || 'user');
              var expandBtn = side === 'assistant' ? '<button class="ctx-expand-btn" data-msg-idx="' + idx + '" data-session-id="' + esc(self._activeSession.id) + '" title="Toggle inline context details">&gt; Context</button>' : '';
              var gradeHtml = '';
              if (side === 'assistant') {
                var turnForMsg = turns[asstCount] || null;
                asstCount++;
                if (turnForMsg) {
                  var existingFb = feedbackByTurn[turnForMsg.id];
                  var existingGrade = existingFb ? existingFb.grade : 0;
                  gradeHtml = '<div class="grade-stars" data-turn-id="' + esc(turnForMsg.id) + '">';
                  for (var g = 1; g <= 5; g++) {
                    gradeHtml += '<span class="star' + (g <= existingGrade ? ' filled' : '') + '" data-grade="' + g + '">\u2605</span>';
                  }
                  gradeHtml += '<button class="grade-comment-toggle" data-turn-id="' + esc(turnForMsg.id) + '">comment</button>';
                  gradeHtml += '</div>';
                  if (existingFb && existingFb.comment) {
                    gradeHtml += '<div style="font-size:0.625rem;color:var(--muted);margin-top:0.125rem">\u201c' + esc(existingFb.comment) + '\u201d</div>';
                  }
                }
              }
              var timeHtml = '';
              if (m.created_at) {
                var d = new Date(m.created_at);
                var now = new Date();
                var diffMs = now - d;
                var diffMin = Math.floor(diffMs / 60000);
                var relative = diffMin < 1 ? 'just now'
                  : diffMin < 60 ? diffMin + 'm ago'
                  : diffMin < 1440 ? Math.floor(diffMin / 60) + 'h ago'
                  : Math.floor(diffMin / 1440) + 'd ago';
                timeHtml = '<div class="msg-time" title="' + esc(d.toISOString()) + '" style="font-size:0.625rem;color:var(--muted);text-align:right;margin-top:0.25rem">' + relative + '</div>';
              }
              return '<div class="message ' + side + '" id="msg-' + idx + '"><div class="message-role">' + esc(roleLabel) + expandBtn + '</div><div>' + renderSafeMarkdown(m.content || '') + '</div>' + gradeHtml + timeHtml + '<div class="ctx-detail" id="ctx-detail-' + idx + '" style="display:none"></div></div>';
            }).join('') || '<p style="color:var(--muted);padding:1rem">Send a message to begin the conversation.</p>';
            var chatLabel = self._activeSession.nickname || truncate(self._activeSession.id, 16);
            var sessionAgentId = (self._activeSession.agent_id || '').toLowerCase();
            // A session belongs to the orchestrator when agent IDs match exactly,
            // or we can't determine the root agent (graceful degradation).
            // Composite IDs like "duncan:automation_scripting" are non-orchestrator.
            var isOrchestratorSession = !rootAgentId || sessionAgentId === rootAgentId;
            var isReadOnly = self._activeSession.non_interactive || !isOrchestratorSession;
            // Non-orchestrator sessions show their own agent name, not the orchestrator's
            var chatAgentLabel = isOrchestratorSession
              ? sessionAssistantLabel(self._activeSession, self._activeSession.agent_id || 'default')
              : (self._activeSession.agent_id || 'subagent');
            var readOnlyReason = self._activeSession.non_interactive
              ? 'cron/subagent session'
              : (!isOrchestratorSession ? 'subagent session' : '');
            var inputArea = isReadOnly
              ? '<div style="padding:0.75rem 1rem;background:rgba(245,158,11,0.08);border-top:1px solid var(--border-ghost);font-size:0.8125rem;color:var(--muted);text-align:center">\ud83d\udd12 Read-only (' + readOnlyReason + ')</div>'
              : '<div class="session-chat-input"><textarea id="session-msg-input" placeholder="Type a message\u2026" rows="1" autocomplete="off"></textarea><button class="btn" id="btn-send-msg">' + uiBtnLabel('send', 'Send') + '</button></div>';
            return '<div class="session-chat-wrap"><div class="session-chat-header"><button class="btn secondary" style="font-size:0.75rem;padding:0.3rem 0.75rem" id="btn-back-sessions">' + uiBtnLabel('back', 'Back') + '</button><span style="font-weight:600">' + esc(chatAgentLabel) + '</span><span class="session-nick" title="' + esc(self._activeSession.id) + '" style="color:var(--muted);font-size:0.75rem">' + esc(chatLabel) + '</span>' + copyIdBtn(self._activeSession.id) + (isReadOnly ? ' <span class="badge" style="font-size:0.5rem;padding:1px 5px;background:var(--warning);color:#000;vertical-align:middle">AUTO</span>' : '') + '</div><div class="message-thread">' + msgs + '</div>' + inputArea + '</div>';
          });
        }

        // Separate active/closed from archived
        var showArchived = self._showArchivedSessions || false;
        var activeSessions = sessions.filter(function(s) { return s.status !== 'archived'; });
        var archivedSessions = sessions.filter(function(s) { return s.status === 'archived'; });
        var displaySessions = showArchived ? sessions : activeSessions;

        var PAGE_SIZE = 25;
        var page = self._sessionsPage || 0;
        var totalPages = Math.ceil(displaySessions.length / PAGE_SIZE) || 1;
        if (page >= totalPages) page = totalPages - 1;
        var paged = displaySessions.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
        var rows = paged.map(function(s) {
          // Fallback label: nickname → agent name for subagents → truncated ID
          var sAgentLower = (s.agent_id || '').toLowerCase();
          var isSubagent = rootAgentId && sAgentLower !== rootAgentId;
          // For composite IDs like "duncan:automation_scripting", show the subagent part
          var subagentLabel = s.agent_id || '';
          if (subagentLabel.indexOf(':') > -1) subagentLabel = subagentLabel.split(':').slice(1).join(':');
          var label = s.nickname || (isSubagent ? subagentLabel : '') || truncate(s.id, 16);
          if (label === 'Untitled' && s.agent_id) label = s.agent_id;
          var st = s.status || 'active';
          var stCls = st === 'active' ? 'success' : (st === 'closed' ? '' : 'warning');
          var isNonInteractive = s.non_interactive;
          var openLabel = isNonInteractive ? 'View' : 'Open';
          var archiveBtn = st === 'archived'
            ? '<button class="btn secondary" style="font-size:0.65rem;padding:0.2rem 0.5rem" disabled>Archived</button>'
            : '<button class="btn secondary session-archive-btn" data-archive-session="' + esc(s.id || '') + '" style="font-size:0.65rem;padding:0.2rem 0.5rem" title="Archive">Archive</button>';
          return '<tr data-id="' + esc(s.id || '') + '" style="cursor:pointer" title="' + esc(s.id || '') + '"><td><span class="session-nick">' + esc(label) + '</span>' + copyIdBtn(s.id || '') + '</td><td>' + esc(s.agent_id || '') + '</td><td><span class="badge ' + stCls + '" style="font-size:0.5625rem">' + esc(st) + '</span></td><td>' + esc(s.created_at || '') + '</td><td>' + esc(s.updated_at || '') + '</td><td style="display:flex;gap:0.25rem"><button class="btn secondary" style="font-size:0.65rem;padding:0.2rem 0.5rem">' + openLabel + '</button>' + archiveBtn + '<button class="btn secondary danger session-delete-btn" data-delete-session="' + esc(s.id || '') + '" style="font-size:0.65rem;padding:0.2rem 0.5rem" title="Delete">\u2715</button></td></tr>';
        }).join('');
        var emptyHint = displaySessions.length === 0 ? '<div class="card" style="margin-top:1rem;color:var(--muted)">No sessions yet. Click <strong>New session</strong> to start one.</div>' : '';
        var pager = totalPages > 1 ? '<div style="display:flex;gap:0.5rem;align-items:center;margin-top:0.75rem;justify-content:center"><button class="btn secondary" id="sess-prev" style="font-size:0.75rem;padding:0.2rem 0.6rem"' + (page === 0 ? ' disabled' : '') + '>\u2190 Prev</button><span style="font-size:0.75rem;color:var(--muted)">Page ' + (page + 1) + ' of ' + totalPages + '</span><button class="btn secondary" id="sess-next" style="font-size:0.75rem;padding:0.2rem 0.6rem"' + (page >= totalPages - 1 ? ' disabled' : '') + '>Next \u2192</button></div>' : '';
        var archiveToggle = archivedSessions.length > 0
          ? '<button class="btn secondary" id="toggle-archived" style="font-size:0.7rem;padding:0.25rem 0.6rem">' + (showArchived ? 'Hide archived (' + archivedSessions.length + ')' : 'Show archived (' + archivedSessions.length + ')') + '</button>'
          : '';
        return '<div style="display:flex;gap:0.75rem;align-items:center"><button class="btn" id="btn-new-session">' + uiBtnLabel('plus', 'New session') + '</button><span style="font-size:0.8125rem;color:var(--muted)">' + activeSessions.length + ' active</span>' + archiveToggle + '</div><div class="table-wrap" style="margin-top:1rem;max-height:calc(100vh - 56px - 8rem);overflow-y:auto"><table><thead><tr><th>Topic</th><th>Agent</th><th>Status</th><th>Created</th><th>Updated</th><th>Action</th></tr></thead><tbody>' + rows + '</tbody></table></div>' + pager + emptyHint;
      });
};