Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions staged/src/lib/features/branches/BranchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -863,8 +863,8 @@
notifyError('Session Error', 'Failed to start session: no session ID returned');
return;
}
// Register session in the unified registry
sessionRegistry.register(result.sessionId, branch.projectId, 'other', branch.id);
// Register session in the unified registry with the actual session type
sessionRegistry.register(result.sessionId, branch.projectId, newSessionMode, branch.id);
projectStateStore.addRunningSession(branch.projectId, result.sessionId);
showNewSession = false;
draftPrompt = '';
Expand Down
6 changes: 3 additions & 3 deletions staged/src/lib/features/branches/RemoteBranchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,9 @@
notifyError('Session Error', 'Failed to start session: no session ID returned');
return;
}
// Register session in the unified registry so global completion handling can
// clear running/unread indicators for remote projects.
sessionRegistry.register(result.sessionId, branch.projectId, 'other', branch.id);
// Register session in the unified registry with the actual session type so global
// completion handling can clear running/unread indicators for remote projects.
sessionRegistry.register(result.sessionId, branch.projectId, newSessionMode, branch.id);
// Track the running session in the project state store
projectStateStore.addRunningSession(branch.projectId, result.sessionId);
const pendingType =
Expand Down
11 changes: 9 additions & 2 deletions staged/src/lib/features/projects/ProjectsList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
import { GitPullRequest, GitPullRequestClosed, GitPullRequestDraft, Plus } from 'lucide-svelte';
import type { Project, ProjectRepo, Branch } from '../../types';
import * as commands from '../../commands';
import { projectDisplayName, aggregateProjectPrStatus } from '../../shared/utils';
import {
projectDisplayName,
aggregateProjectPrStatus,
projectSubtitle,
} from '../../shared/utils';
import { projectStateStore } from '../../stores/projectState.svelte';
import { selectProject } from '../../navigation.svelte';
import NewProjectModal from './NewProjectModal.svelte';
import ProjectsSidebar from './ProjectsSidebar.svelte';
Expand Down Expand Up @@ -281,6 +286,8 @@
{@const status = getProjectStatus(project.id, deletingProjectNames)}
{@const prStatus = getProjectPrStatus(project.id)}
{@const repos = reposByProject.get(project.id) ?? []}
{@const repoCount = repoCountsByProject.get(project.id) ?? (project.githubRepo ? 1 : 0)}
{@const sessionTypes = projectStateStore.getRunningSessionTypes(project.id)}
<div class="project-card-wrapper">
<button
class="project-card"
Expand Down Expand Up @@ -336,7 +343,7 @@
</div>
</button>
<div class="card-location">
{project.location === 'remote' ? 'Remote workspace' : 'Local worktrees'}
{projectSubtitle(repoCount, sessionTypes)}
</div>
</div>
{/each}
Expand Down
12 changes: 8 additions & 4 deletions staged/src/lib/features/projects/ProjectsSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
} from 'lucide-svelte';
import type { Project, Branch } from '../../types';
import { goHome, navigation, selectProject } from '../../navigation.svelte';
import { projectDisplayName, aggregateProjectPrStatus } from '../../shared/utils';
import {
projectDisplayName,
aggregateProjectPrStatus,
projectSubtitle,
} from '../../shared/utils';
import { projectStateStore } from '../../stores/projectState.svelte';
import Spinner from '../../shared/Spinner.svelte';
import StagedIcon from '../../shared/StagedIcon.svelte';
import { getProjectStatus } from './projectStatus';
Expand Down Expand Up @@ -193,6 +198,7 @@
{@const status = getProjectStatus(project.id, deletingProjectNames)}
{@const repoCount = repoCountForProject(project)}
{@const prStatus = getProjectPrStatus(project.id)}
{@const sessionTypes = projectStateStore.getRunningSessionTypes(project.id)}
<button
class="project-row"
class:active={navigation.selectedProjectId === project.id}
Expand All @@ -218,9 +224,7 @@
<div class="row-text">
<span class="project-name">{projectDisplayName(project)}</span>
<div class="row-meta">
<span class="repo-count"
>{repoCount} {repoCount === 1 ? 'repo' : 'repos'}</span
>
<span class="repo-count">{projectSubtitle(repoCount, sessionTypes)}</span>
</div>
</div>
</div>
Expand Down
93 changes: 93 additions & 0 deletions staged/src/lib/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import type { Project, Branch } from '../types';
import type { SessionType } from '../stores/sessionRegistry.svelte';

/** Display name for a project: repo basename + optional subpath. */
export function projectDisplayName(p: Project): string {
Expand Down Expand Up @@ -86,3 +87,95 @@ export function aggregateProjectPrStatus(

return null;
}

/**
* Human-readable label for a session type.
* Returns the singular noun form (e.g. "commit", "note").
*/
function sessionTypeLabel(type: SessionType): string {
switch (type) {
case 'commit':
return 'commit';
case 'note':
return 'note';
case 'review':
return 'review';
case 'pr':
return 'PR';
case 'push':
return 'push';
case 'other':
return 'task';
}
}

/**
* Pluralize a noun: adds "s" when count !== 1, with special handling for
* irregular forms used in session labels.
*/
function pluralize(word: string, count: number): string {
if (count === 1) return word;
// "PR" → "PRs", everything else just gets an "s"
return `${word}s`;
}

/**
* Build a project subtitle that summarizes the repo count and any running
* session activity.
*
* Examples:
* "1 repo" (idle)
* "2 repos" (idle)
* "1 repo · making a commit"
* "2 repos · making a commit and a note"
* "1 repo · making commits and notes"
* "2 repos · pushing changes"
*/
export function projectSubtitle(repoCount: number, sessionTypes: SessionType[]): string {
const repoLabel = `${repoCount} ${pluralize('repo', repoCount)}`;

if (sessionTypes.length === 0) {
return repoLabel;
}

// Count occurrences of each session type
const counts = new Map<SessionType, number>();
for (const type of sessionTypes) {
counts.set(type, (counts.get(type) ?? 0) + 1);
}

// Build the activity description from the counted types.
// Use a stable display order so the subtitle doesn't jump around.
const displayOrder: SessionType[] = ['commit', 'note', 'review', 'pr', 'push', 'other'];
const parts: string[] = [];

for (const type of displayOrder) {
const count = counts.get(type);
if (!count) continue;

const label = sessionTypeLabel(type);
if (count === 1) {
parts.push(`a ${label}`);
} else {
parts.push(pluralize(label, count));
}
}

// Join with commas and "and": "a commit", "a commit and a note",
// "commits, notes, and a review"
let activity: string;
if (parts.length === 1) {
activity = parts[0];
} else if (parts.length === 2) {
activity = `${parts[0]} and ${parts[1]}`;
} else {
activity = `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}`;
}

// Special-case "push" to read more naturally: "pushing changes" instead of "making a push"
if (counts.size === 1 && counts.has('push')) {
return `${repoLabel} · pushing changes`;
}

return `${repoLabel} · making ${activity}`;
}
22 changes: 21 additions & 1 deletion staged/src/lib/stores/projectState.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* Note: Session-to-project lookups are now delegated to the unified sessionRegistry
*/

import { sessionRegistry } from './sessionRegistry.svelte';
import { sessionRegistry, type SessionType } from './sessionRegistry.svelte';

interface ProjectState {
unread: boolean;
Expand Down Expand Up @@ -170,6 +170,26 @@ class ProjectStateStore {
const state = this.states.get(projectId);
return state ? state.runningSessions.size : 0;
}

/**
* Get the session types of all running sessions for a project.
* Returns an array of SessionType values (one per running session).
* Delegates type lookups to the unified session registry.
*/
getRunningSessionTypes(projectId: string): SessionType[] {
// Access version to establish reactive dependency
this.version;
const state = this.getState(projectId);
if (!state) return [];
const types: SessionType[] = [];
for (const sessionId of state.runningSessions) {
const type = sessionRegistry.getType(sessionId);
if (type) {
types.push(type);
}
}
return types;
}
}

export const projectStateStore = new ProjectStateStore();