Skip to content

Commit 3377b4f

Browse files
committed
feat: activity actions + more events
1 parent 027888a commit 3377b4f

3 files changed

Lines changed: 136 additions & 0 deletions

File tree

client/activity-feed.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,24 +280,92 @@ class ActivityFeedPanel {
280280
const summary = this.escapeHtml(this.summarizeEvent(ev));
281281
const dataJson = this.escapeHtml(this.compactJson(ev?.data));
282282
const group = this.getGroup(kind);
283+
const actions = this.renderEventActions(ev);
283284

284285
return `
285286
<div class="activity-event">
286287
<div class="activity-meta">
287288
<span class="activity-time">${this.escapeHtml(time)}</span>
288289
<span class="activity-kind activity-kind-${this.escapeHtml(group)}">${this.escapeHtml(kind)}</span>
290+
${actions}
289291
</div>
290292
<div class="activity-summary">${summary}</div>
291293
<div class="activity-data">${dataJson}</div>
292294
</div>
293295
`;
294296
}
295297

298+
renderEventActions(ev) {
299+
const data = ev?.data && typeof ev.data === 'object' ? ev.data : {};
300+
const sessionId = String(data.sessionId || '').trim();
301+
const url = String(data.url || '').trim();
302+
303+
const parts = [];
304+
if (sessionId) {
305+
parts.push(`<button class="btn-secondary activity-action-btn" onclick="event.stopPropagation(); window.activityFeedPanel.handleEventAction('focus', '${this.escapeHtml(ev.id)}')">Focus</button>`);
306+
}
307+
if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
308+
parts.push(`<button class="btn-secondary activity-action-btn" onclick="event.stopPropagation(); window.activityFeedPanel.handleEventAction('open', '${this.escapeHtml(ev.id)}')">Open</button>`);
309+
}
310+
parts.push(`<button class="btn-secondary activity-action-btn" onclick="event.stopPropagation(); window.activityFeedPanel.handleEventAction('copy', '${this.escapeHtml(ev.id)}')">Copy</button>`);
311+
312+
return `<div class="activity-actions">${parts.join('')}</div>`;
313+
}
314+
315+
async handleEventAction(action, eventId) {
316+
const id = String(eventId || '').trim();
317+
if (!id) return;
318+
const ev = this.events.find((e) => e && e.id === id) || null;
319+
if (!ev) return;
320+
321+
const data = ev?.data && typeof ev.data === 'object' ? ev.data : {};
322+
const sessionId = String(data.sessionId || '').trim();
323+
const url = String(data.url || '').trim();
324+
325+
try {
326+
if (action === 'focus' && sessionId) {
327+
this.orchestrator?.focusTerminal?.(sessionId);
328+
return;
329+
}
330+
331+
if (action === 'open' && url) {
332+
window.open(url, '_blank');
333+
return;
334+
}
335+
336+
if (action === 'copy') {
337+
const text = JSON.stringify(ev, null, 2);
338+
try {
339+
await navigator.clipboard.writeText(text);
340+
} catch {
341+
const ta = document.createElement('textarea');
342+
ta.value = text;
343+
ta.style.position = 'fixed';
344+
ta.style.left = '-9999px';
345+
document.body.appendChild(ta);
346+
ta.select();
347+
document.execCommand('copy');
348+
ta.remove();
349+
}
350+
}
351+
} catch {
352+
// ignore
353+
}
354+
}
355+
296356
summarizeEvent(ev) {
297357
const kind = String(ev?.kind || '');
298358
const data = ev?.data && typeof ev.data === 'object' ? ev.data : {};
299359

300360
if (kind === 'server.started') return `Server started (port ${data.port || 'unknown'})`;
361+
if (kind === 'workspace.switch.requested') return `Workspace switch requested (${data.fromWorkspaceId || '?'}${data.toWorkspaceId || '?'})`;
362+
if (kind === 'workspace.switch.completed') return `Workspace switched (${data.fromWorkspaceId || '?'}${data.toWorkspaceName || data.toWorkspaceId || '?'})`;
363+
if (kind === 'workspace.switch.failed') return `Workspace switch failed (${data.toWorkspaceId || '?'})`;
364+
if (kind === 'worktree.sessions.add.requested') return `Add worktree sessions requested (${data.repositoryName ? `${data.repositoryName}/` : ''}${data.worktreeId || '?'})`;
365+
if (kind === 'worktree.sessions.add.completed') return `Worktree sessions added (${data.worktreeId || '?'})`;
366+
if (kind === 'worktree.sessions.add.failed') return `Add worktree sessions failed (${data.worktreeId || '?'})`;
367+
if (kind === 'tab.closed') return `Tab closed (${data.tabId || '?'}, closed ${data.closed || 0})`;
368+
if (kind === 'session.closed') return `Session closed (${data.sessionId || '?'})`;
301369
if (kind === 'git.pull') return `Git pull (${data.ok ? 'ok' : 'failed'})`;
302370
if (kind === 'pr.merge') return `PR merge ${data.ok ? 'ok' : 'failed'} (${data.repo || 'repo'} #${data.prNumber || '?'})`;
303371
if (kind === 'pr.review') return `PR review ${data.ok ? 'ok' : 'failed'} (${data.repo || 'repo'} #${data.prNumber || '?'})`;

client/styles.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9931,6 +9931,19 @@ header h1 {
99319931
justify-content: space-between;
99329932
}
99339933

9934+
.activity-actions {
9935+
display: flex;
9936+
gap: var(--space-xs);
9937+
align-items: center;
9938+
margin-left: auto;
9939+
}
9940+
9941+
.activity-action-btn {
9942+
padding: 4px 8px;
9943+
font-size: 0.75rem;
9944+
line-height: 1;
9945+
}
9946+
99349947
.activity-time {
99359948
color: var(--text-muted);
99369949
font-size: 0.8rem;

server/index.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,13 @@ io.on('connection', (socket) => {
614614
// Workspace management handlers
615615
socket.on('switch-workspace', async ({ workspaceId }) => {
616616
try {
617+
const previous = workspaceManager.getActiveWorkspace?.() || null;
618+
activityFeed.track('workspace.switch.requested', {
619+
fromWorkspaceId: previous?.id || null,
620+
toWorkspaceId: String(workspaceId || '').trim() || null,
621+
socketId: socket.id
622+
});
623+
617624
logger.info('Workspace switch requested', { workspaceId });
618625

619626
const newWorkspace = await workspaceManager.switchWorkspace(workspaceId);
@@ -649,7 +656,18 @@ io.on('connection', (socket) => {
649656
}
650657

651658
logger.info('Workspace switched successfully', { workspace: newWorkspace.name });
659+
activityFeed.track('workspace.switch.completed', {
660+
fromWorkspaceId: previous?.id || null,
661+
toWorkspaceId: newWorkspace?.id || null,
662+
toWorkspaceName: newWorkspace?.name || null,
663+
socketId: socket.id
664+
});
652665
} catch (error) {
666+
activityFeed.track('workspace.switch.failed', {
667+
toWorkspaceId: String(workspaceId || '').trim() || null,
668+
socketId: socket.id,
669+
error: error.message
670+
});
653671
logger.error('Failed to switch workspace', { error: error.message, stack: error.stack });
654672
socket.emit('error', { message: 'Failed to switch workspace', error: error.message, stack: error.stack });
655673
}
@@ -673,6 +691,14 @@ io.on('connection', (socket) => {
673691
socket.on('add-worktree-sessions', async ({ worktreeId, worktreePath, repositoryName, repositoryType, repositoryRoot, startTier }) => {
674692
try {
675693
logger.info('Adding sessions for new worktree', { worktreeId, worktreePath, repositoryName });
694+
activityFeed.track('worktree.sessions.add.requested', {
695+
worktreeId: String(worktreeId || '').trim() || null,
696+
worktreePath: String(worktreePath || '').trim() || null,
697+
repositoryName: String(repositoryName || '').trim() || null,
698+
repositoryType: String(repositoryType || '').trim() || null,
699+
startTier: startTier === undefined ? null : Number(startTier),
700+
socketId: socket.id
701+
});
676702

677703
// Create sessions for just this worktree
678704
const newSessions = await sessionManager.createSessionsForWorktree({
@@ -754,7 +780,19 @@ io.on('connection', (socket) => {
754780
worktreeId,
755781
sessionCount: Object.keys(newSessions).length
756782
});
783+
activityFeed.track('worktree.sessions.add.completed', {
784+
worktreeId: String(worktreeId || '').trim() || null,
785+
sessionCount: Object.keys(newSessions).length,
786+
socketId: socket.id
787+
});
757788
} catch (error) {
789+
activityFeed.track('worktree.sessions.add.failed', {
790+
worktreeId: String(worktreeId || '').trim() || null,
791+
worktreePath: String(worktreePath || '').trim() || null,
792+
repositoryName: String(repositoryName || '').trim() || null,
793+
socketId: socket.id,
794+
error: error.message
795+
});
758796
logger.error('Failed to add worktree sessions', { worktreeId, error: error.message });
759797
socket.emit('error', { message: 'Failed to add worktree sessions', error: error.message });
760798
}
@@ -777,7 +815,18 @@ io.on('connection', (socket) => {
777815
}
778816

779817
logger.info('Tab closed', { tabId, closed });
818+
activityFeed.track('tab.closed', {
819+
tabId: String(tabId || '').trim() || null,
820+
closed,
821+
sessionCount: ids.length,
822+
socketId: socket.id
823+
});
780824
} catch (error) {
825+
activityFeed.track('tab.close.failed', {
826+
tabId: String(tabId || '').trim() || null,
827+
socketId: socket.id,
828+
error: error.message
829+
});
781830
logger.error('Failed to close tab', { tabId, error: error.message });
782831
}
783832
});
@@ -791,7 +840,13 @@ io.on('connection', (socket) => {
791840
if (ok) {
792841
io.emit('session-closed', { sessionId: id });
793842
}
843+
activityFeed.track('session.closed', { sessionId: id, ok: !!ok, socketId: socket.id });
794844
} catch (error) {
845+
activityFeed.track('session.close.failed', {
846+
sessionId: String(sessionId || '').trim() || null,
847+
socketId: socket.id,
848+
error: error.message
849+
});
795850
logger.error('Failed to destroy session', { sessionId, error: error.message });
796851
}
797852
});

0 commit comments

Comments
 (0)