Skip to content

Commit 5444bd3

Browse files
committed
feat(dashboard): pool hot tickets across projects
Add buildHotTickets + an onHotTickets telefunc that pool every project's tickets/ and bucket each into a lane: in-progress (planned or spiked), next (high priority, not started), or queued (the rest). There is no run to ticket link, so a ticket being implemented right now is only visible through the plan/spike it left. Exported through the package surface and the client shim; the telefunc auto-registers.
1 parent 2581fce commit 5444bd3

7 files changed

Lines changed: 116 additions & 7 deletions

File tree

packages/framework-dashboard/server/reads.telefunc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
// Imported then exported, not re-exported (#1014): telefunc's dev transform appends
66
// `__decorateTelefunction(<name>, ...)` per export, which needs a local binding. An
77
// `export ... from` creates none, so `pnpm dev` died with `<name> is not defined`.
8-
import { onRuns, onRun, onDocs, onProjectLog, onQueue, onOverview, onRecentRuns, onInterventions, onActivity, onDashboard, onGithubUrl, onGitStatus, onProjectFiles, onProjectFileStatus, onFileDiff, onRunChanges, onFileContent, onTickets, onRetainedWorktrees, onRunWorktree, onRunHandoff, onSystemPromptUser } from '@gemstack/the-framework/dashboard-rpc'
8+
import { onRuns, onRun, onDocs, onProjectLog, onQueue, onOverview, onRecentRuns, onHotTickets, onInterventions, onActivity, onDashboard, onGithubUrl, onGitStatus, onProjectFiles, onProjectFileStatus, onFileDiff, onRunChanges, onFileContent, onTickets, onRetainedWorktrees, onRunWorktree, onRunHandoff, onSystemPromptUser } from '@gemstack/the-framework/dashboard-rpc'
99

10-
export { onRuns, onRun, onDocs, onProjectLog, onQueue, onOverview, onRecentRuns, onInterventions, onActivity, onDashboard, onGithubUrl, onGitStatus, onProjectFiles, onProjectFileStatus, onFileDiff, onRunChanges, onFileContent, onTickets, onRetainedWorktrees, onRunWorktree, onRunHandoff, onSystemPromptUser }
10+
export { onRuns, onRun, onDocs, onProjectLog, onQueue, onOverview, onRecentRuns, onHotTickets, onInterventions, onActivity, onDashboard, onGithubUrl, onGitStatus, onProjectFiles, onProjectFileStatus, onFileDiff, onRunChanges, onFileContent, onTickets, onRetainedWorktrees, onRunWorktree, onRunHandoff, onSystemPromptUser }

packages/the-framework/src/dashboard-rpc/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// implementations live here in @gemstack/the-framework so `sendStart` (added with the serve
33
// wiring) can reach the daemon's `startRun`; the framework-dashboard client imports
44
// these through thin re-export shims so the baked RPC keys stay `/server/*.telefunc.ts`.
5-
export { onRuns, onRun, onDocs, onProjectLog, onQueue, onOverview, onRecentRuns, onInterventions, onActivity, onDashboard, onGithubUrl, onGitStatus, onProjectFiles, onProjectFileStatus, onFileDiff, onRunChanges, onFileContent, onTickets, onRetainedWorktrees, onRunWorktree, onRunHandoff, onSystemPromptUser } from './reads.telefunc.js'
5+
export { onRuns, onRun, onDocs, onProjectLog, onQueue, onOverview, onRecentRuns, onHotTickets, onInterventions, onActivity, onDashboard, onGithubUrl, onGitStatus, onProjectFiles, onProjectFileStatus, onFileDiff, onRunChanges, onFileContent, onTickets, onRetainedWorktrees, onRunWorktree, onRunHandoff, onSystemPromptUser } from './reads.telefunc.js'
66
export { sendStop, sendChoice, sendMessage, sendSetHandoff, sendStart, sendPreview, onServeTargets, sendStopPreview, onPreviewStatus, sendOpenInApp, sendRemoveWorktree, sendDeleteSession, sendPushBranch, sendOpenPullRequest, sendQueueTicket, type QueueTicketResult } from './control.telefunc.js'
77
export { onEvents } from './events.telefunc.js'
88
export { onProjects, sendAddProject, onOnboarding } from './projects.telefunc.js'

packages/the-framework/src/dashboard-rpc/reads.telefunc.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { readLogs, type LogEntry } from '../logs.js'
55
import { readDocs, type WorkspaceDoc } from '../dashboard/docs.js'
66
import { readTickets, type WorkspaceTicket } from '../dashboard/tickets.js'
77
import { collectQueue, type ProjectQueue } from '../dashboard/queue.js'
8-
import { buildOverview, buildRecentRuns, type Overview, type RecentRun } from '../dashboard/overview.js'
8+
import { buildOverview, buildRecentRuns, buildHotTickets, type Overview, type RecentRun, type HotTicket } from '../dashboard/overview.js'
99
import { buildInterventions, type Intervention } from '../dashboard/interventions.js'
1010
import { buildActivity, type Activity } from '../dashboard/activity.js'
1111
import { buildDashboard, type DashboardData } from '../dashboard/dashboard.js'
@@ -182,6 +182,11 @@ export async function onRecentRuns(): Promise<RecentRun[]> {
182182
return withProjects(projects => buildRecentRuns(projects))
183183
}
184184

185+
/** Hot tickets across every project (#1112): being worked on, likely next, and queued. */
186+
export async function onHotTickets(): Promise<HotTicket[]> {
187+
return withProjects(projects => buildHotTickets(projects))
188+
}
189+
185190
/** The cross-project interventions queue (#632, Queue #624): open PRs that need review, newest first. */
186191
export async function onInterventions(): Promise<Intervention[]> {
187192
return withProjects(buildInterventions)

packages/the-framework/src/dashboard/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export { serveClientBundle } from './static.js'
1515
export { readDocs, DOC_CATEGORIES, type WorkspaceDoc } from './docs.js'
1616
export { readTickets, type WorkspaceTicket } from './tickets.js'
1717
export { collectQueue, parseTodoItems, type ProjectQueue, type QueueItem } from './queue.js'
18-
export { buildOverview, buildRecentRuns, type Overview, type ActiveRun, type RecentProject, type RecentRun, type OverviewDeps } from './overview.js'
18+
export { buildOverview, buildRecentRuns, buildHotTickets, ticketBucket, type Overview, type ActiveRun, type RecentProject, type RecentRun, type HotTicket, type HotBucket, type OverviewDeps } from './overview.js'
1919
export { buildDashboard, type DashboardData, type ProjectStat, type ActivityDay, type DashboardDeps } from './dashboard.js'
2020
export { readGitStatus, type GitStatus } from './git-status.js'
2121
export { ghPrView, ghPrList, ghJson, nodeGhRunner, type LinkedPr, type OpenPr, type PrLookup, type BranchPrLookup, type PrLister, type GhRunner } from './gh.js'

packages/the-framework/src/dashboard/overview.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { strict as assert } from 'node:assert'
22
import { test } from 'node:test'
3-
import { buildOverview, buildRecentRuns } from './overview.js'
3+
import { buildOverview, buildRecentRuns, buildHotTickets, ticketBucket } from './overview.js'
44
import type { ProjectSummary } from './projects.js'
55
import type { ProjectQueue } from './queue.js'
6+
import type { WorkspaceTicket } from './tickets.js'
67
import type { RunMeta } from '../store/index.js'
78

89
const project = (id: string, path: string, lastActivityAt?: string): ProjectSummary => ({
@@ -91,3 +92,51 @@ test('buildRecentRuns tolerates a project whose runs cannot be read', async () =
9192
})
9293
assert.deepEqual(recent.map(r => r.run.id), ['x'])
9394
})
95+
96+
const ticket = (file: string, over: Partial<WorkspaceTicket> = {}): WorkspaceTicket => ({
97+
file,
98+
title: file,
99+
summary: '',
100+
spiked: false,
101+
planned: false,
102+
...over,
103+
})
104+
105+
test('ticketBucket: planned/spiked is in-progress, high priority is next, else queued', () => {
106+
assert.equal(ticketBucket(ticket('a', { planned: true })), 'in-progress')
107+
assert.equal(ticketBucket(ticket('b', { spiked: true })), 'in-progress')
108+
assert.equal(ticketBucket(ticket('c', { priority: 'high' })), 'next')
109+
assert.equal(ticketBucket(ticket('d', { priority: 'p1' })), 'next')
110+
assert.equal(ticketBucket(ticket('e')), 'queued')
111+
assert.equal(ticketBucket(ticket('f', { priority: 'low' })), 'queued')
112+
// A planned high-prio ticket is in-progress, not next: work already started outranks the flag.
113+
assert.equal(ticketBucket(ticket('g', { planned: true, priority: 'high' })), 'in-progress')
114+
})
115+
116+
test('buildHotTickets pools every project, buckets each, and orders lane-first', async () => {
117+
const tickets: Record<string, WorkspaceTicket[]> = {
118+
'/a': [ticket('a1.md', { planned: true }), ticket('a2.md', { priority: 'high' })],
119+
'/b': [ticket('b1.md')],
120+
}
121+
const hot = await buildHotTickets([project('alpha', '/a'), project('beta', '/b')], {
122+
tickets: async cwd => tickets[cwd] ?? [],
123+
})
124+
assert.deepEqual(
125+
hot.map(h => ({ p: h.projectName, f: h.ticket.file, b: h.bucket })),
126+
[
127+
{ p: 'alpha', f: 'a1.md', b: 'in-progress' },
128+
{ p: 'alpha', f: 'a2.md', b: 'next' },
129+
{ p: 'beta', f: 'b1.md', b: 'queued' },
130+
],
131+
)
132+
})
133+
134+
test('buildHotTickets tolerates a project whose tickets cannot be read', async () => {
135+
const hot = await buildHotTickets([project('ok', '/ok'), project('bad', '/bad')], {
136+
tickets: async cwd => {
137+
if (cwd === '/bad') throw new Error('unreadable')
138+
return [ticket('x.md', { priority: 'high' })]
139+
},
140+
})
141+
assert.deepEqual(hot.map(h => h.ticket.file), ['x.md'])
142+
})

packages/the-framework/src/dashboard/overview.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readAllRuns, readLiveMetas, type LiveRun, type RunMeta, type RunStatus } from '../store/index.js'
22
import type { ProjectSummary } from './projects.js'
33
import { collectQueue, type ProjectQueue } from './queue.js'
4+
import { readTickets, type WorkspaceTicket } from './tickets.js'
45

56
// The first-sidebar Overview (#437, part of #314): a cross-project glance at what the agent
67
// is working on right now, the size of the backlog, and the recently active projects. It
@@ -81,6 +82,60 @@ export async function buildRecentRuns(projects: ProjectSummary[], deps: RecentRu
8182
return all.slice(0, RECENT_RUNS_LIMIT)
8283
}
8384

85+
/** Which lane of the "hot tickets" overview (#1112) a ticket sits in. */
86+
export type HotBucket = 'in-progress' | 'next' | 'queued'
87+
88+
/** One ticket surfaced on the Overview's hot-tickets card, tagged with its project and lane. */
89+
export interface HotTicket {
90+
projectId: string
91+
projectName: string
92+
bucket: HotBucket
93+
ticket: WorkspaceTicket
94+
}
95+
96+
/** Priority values that read as "do this soon" — the "likely next" lane (#1112). */
97+
const HIGH_PRIORITY = new Set(['high', 'urgent', 'critical', 'p0', 'p1', '0', '1'])
98+
99+
/**
100+
* A ticket's lane (#1112):
101+
* - in-progress: the agent has planned or spiked it, i.e. work is under way. (There is no run↔ticket
102+
* link, so a ticket being *implemented* right now is only visible through the plan/spike it left.)
103+
* - next: no plan/spike yet, but flagged high priority — likely the next thing picked up.
104+
* - queued: everything else open, the backlog waiting its turn.
105+
*/
106+
export function ticketBucket(ticket: WorkspaceTicket): HotBucket {
107+
if (ticket.planned || ticket.spiked) return 'in-progress'
108+
if (ticket.priority && HIGH_PRIORITY.has(ticket.priority)) return 'next'
109+
return 'queued'
110+
}
111+
112+
/** How many hot tickets the Overview pools before the card trims per lane. */
113+
const HOT_TICKETS_LIMIT = 60
114+
115+
/** Injectable reader so {@link buildHotTickets} is unit-testable off disk. */
116+
export interface HotTicketsDeps {
117+
tickets?: (cwd: string) => Promise<WorkspaceTicket[]>
118+
}
119+
120+
/**
121+
* Every project's tickets pooled and bucketed for the Overview's "hot tickets" card (#1112): what
122+
* is being worked on (planned/spiked), what is likely next (high priority), and the queued rest.
123+
* Ordered lane-first (in-progress, then next, then queued), file order within a lane. Forgiving —
124+
* a project whose tickets cannot be read simply contributes nothing.
125+
*/
126+
export async function buildHotTickets(projects: ProjectSummary[], deps: HotTicketsDeps = {}): Promise<HotTicket[]> {
127+
const readT = deps.tickets ?? readTickets
128+
const all: HotTicket[] = []
129+
for (const project of projects) {
130+
for (const ticket of await readT(project.path).catch(() => [])) {
131+
all.push({ projectId: project.id, projectName: project.name, bucket: ticketBucket(ticket), ticket })
132+
}
133+
}
134+
const lane: Record<HotBucket, number> = { 'in-progress': 0, next: 1, queued: 2 }
135+
all.sort((a, b) => lane[a.bucket] - lane[b.bucket])
136+
return all.slice(0, HOT_TICKETS_LIMIT)
137+
}
138+
84139
/** Injectable readers so {@link buildOverview} is unit-testable off disk. */
85140
export interface OverviewDeps {
86141
liveRuns?: (cwd: string) => Promise<LiveRun[]>

packages/the-framework/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export {
130130
type SweepDeps,
131131
type MaintenanceFs,
132132
} from './maintenance.js'
133-
export { startDashboard, summarizeProject, defaultProjectsProvider, readDocs, type Dashboard, type DashboardOptions, type StartRunKind, type StartRunResult, type AddProjectResult, type OnboardingSuggestion, type PreviewResult, type PreviewStatus, type RunWorktree, type ProjectSummary, type ProjectsProvider, type SummarizeDeps, type WorkspaceDoc, readTickets, type WorkspaceTicket, type ProjectQueue, type QueueItem, type Overview, type ActiveRun, type RecentProject, type RecentRun, buildRecentRuns, type DashboardData, type ProjectStat, type ActivityDay, type GitStatus, type LinkedPr, type FileDiff, type FileChange, type FileContent, type RunHandoff, type HandoffCommit, type HandoffFile, type HandoffResult, buildInterventions, type Intervention, type OpenPr, type PrLister, type InterventionsDeps, buildActivity, activityKey, pickNewActivity, type Activity, type ActivityDeps } from './dashboard/index.js'
133+
export { startDashboard, summarizeProject, defaultProjectsProvider, readDocs, type Dashboard, type DashboardOptions, type StartRunKind, type StartRunResult, type AddProjectResult, type OnboardingSuggestion, type PreviewResult, type PreviewStatus, type RunWorktree, type ProjectSummary, type ProjectsProvider, type SummarizeDeps, type WorkspaceDoc, readTickets, type WorkspaceTicket, type ProjectQueue, type QueueItem, type Overview, type ActiveRun, type RecentProject, type RecentRun, buildRecentRuns, type HotTicket, type HotBucket, buildHotTickets, type DashboardData, type ProjectStat, type ActivityDay, type GitStatus, type LinkedPr, type FileDiff, type FileChange, type FileContent, type RunHandoff, type HandoffCommit, type HandoffFile, type HandoffResult, buildInterventions, type Intervention, type OpenPr, type PrLister, type InterventionsDeps, buildActivity, activityKey, pickNewActivity, type Activity, type ActivityDeps } from './dashboard/index.js'
134134
export { startPreview, detectDevScript, detectServeTargets, parsePreviewUrl, PREVIEW_SCRIPTS, type PreviewHandle, type StartPreviewOptions, type ServeTarget } from './preview.js'
135135
export {
136136
RunStore,

0 commit comments

Comments
 (0)