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
48 changes: 48 additions & 0 deletions src/server/queue-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,54 @@ describe('buildQueueState', () => {
expect(perItem).toBeDefined()
expect(perItem?.op).toMatchObject({ action: 'close', number: 5 })
expect(perItem?.filePath).toBe(filePath)
expect(perItem?.title).toBe('Sample')
})

it('populates entry.title from syncState for yml entries', async () => {
const dir = await createTempDir()
await writeFile(join(dir, 'execute.yml'), '- action: close\n number: 7\n', 'utf8')

await saveSyncState(dir, {
version: 2,
repo: 'owner/repo',
items: {
7: {
number: 7,
kind: 'issue',
state: 'open',
lastUpdatedAt: '2026-01-01T00:00:00Z',
lastSyncedAt: '2026-01-01T00:00:00Z',
filePath: 'issues/00007-needs-cleanup.md',
data: {
item: {
number: 7,
kind: 'issue',
state: 'open',
updatedAt: '2026-01-01T00:00:00Z',
createdAt: '2026-01-01T00:00:00Z',
closedAt: null,
title: 'Needs cleanup',
body: null,
author: 'alice',
labels: [],
assignees: [],
milestone: null,
},
comments: [],
},
},
},
executions: [],
})

const queue = await buildQueueState({
storageDirAbsolute: dir,
executeFilePath: join(dir, 'execute.yml'),
})

const entry = queue.entries.find(e => e.op.number === 7)
expect(entry).toBeDefined()
expect(entry?.title).toBe('Needs cleanup')
})

it('assigns stable ids based on content and source', async () => {
Expand Down
10 changes: 6 additions & 4 deletions src/server/queue-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ export async function buildQueueState(options: BuildQueueStateOptions): Promise<
])

const entries: QueueEntry[] = []
const titleOf = (op: PendingOp): string | undefined => syncState.items[String(op.number)]?.data.item.title
for (const [index, op] of yml.ops.entries())
entries.push(buildEntry(op, 'execute.yml', index))
entries.push(buildEntry(op, 'execute.yml', index, undefined, titleOf(op)))
for (const [index, op] of md.ops.entries())
entries.push(buildEntry(op, 'execute.md', index))
entries.push(buildEntry(op, 'execute.md', index, undefined, titleOf(op)))
for (const [index, op] of perItem.ops.entries()) {
const filePath = syncState.items[String(op.number)]?.filePath
entries.push(buildEntry(op, 'per-item', index, filePath))
entries.push(buildEntry(op, 'per-item', index, filePath, titleOf(op)))
}

return {
Expand All @@ -58,12 +59,13 @@ async function readMdOps(path: string): Promise<{ ops: PendingOp[], warnings: st
return { ops: parsed.ops, warnings: parsed.warnings }
}

function buildEntry(op: PendingOp, source: QueueSource, index: number, filePath?: string): QueueEntry {
function buildEntry(op: PendingOp, source: QueueSource, index: number, filePath?: string, title?: string): QueueEntry {
return {
id: hash({ source, index, action: op.action, number: op.number }),
source,
index,
op,
filePath,
title,
}
}
2 changes: 2 additions & 0 deletions src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface QueueEntry {
op: PendingOp
/** Relative file path under the storage directory, for per-item entries. */
filePath?: string
/** Title of the target issue/PR, looked up from syncState. Absent if the item isn't synced. */
title?: string
}

export interface QueueState {
Expand Down
32 changes: 9 additions & 23 deletions ui/components/hub/QueueList.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
<script setup lang="ts">
import type { QueueEntry } from '#ghfs/server-types'
import { ACTIONS_COLOR_HEX } from '#ghfs/action-colors'
import { useRouter } from '#imports'
import { useAppState } from '../../composables/useAppState'
import { useHubQueue } from '../../composables/useHubQueue'
import { useOnlineState } from '../../composables/useOnlineState'
import { useRpc } from '../../composables/useRpc'
import { summarizeQueueOp } from '../../utils/queueSummary'
import DisplayProjectIcon from '../display/ProjectIcon.vue'
import QueueEntryCard from '../queue/EntryCard.vue'
import UiEmptyState from '../ui/EmptyState.vue'
import UiIconButton from '../ui/IconButton.vue'

const hubQueue = useHubQueue()
const router = useRouter()
const rpc = useRpc()
const state = useAppState()
const { offline } = useOnlineState()

function actionColor(action: string): string {
return (ACTIONS_COLOR_HEX as Record<string, string>)[action] ?? '#6b7280'
}

function openItem(repo: string, number: number) {
router.push(`/${repo}/${number}`)
}
Expand Down Expand Up @@ -69,28 +63,20 @@ async function remove(projectId: string, entry: QueueEntry) {
<span>Execute</span>
</button>
</header>
<ul class="flex flex-col divide-y divide-#8882">
<ul class="flex flex-col gap-2 px-4 py-3">
<li
v-for="entry in group.queue.entries.filter((e: QueueEntry) => e.source !== 'per-item')"
v-for="entry in group.queue.entries"
:key="entry.id"
class="group flex items-start gap-2 px-4 py-2 hover:bg-active transition cursor-pointer"
class="group cursor-pointer"
data-testid="hub-queue-entry"
:data-entry-id="entry.id"
@click="openItem(group.repo, entry.op.number)"
>
<span
class="badge font-mono text-xs flex-none"
:style="{ backgroundColor: `${actionColor(entry.op.action)}22`, color: actionColor(entry.op.action) }"
>{{ entry.op.action }}</span>
<span class="font-mono text-xs color-muted tabular-nums flex-none">#{{ entry.op.number }}</span>
<span class="text-xs color-muted truncate flex-1">{{ summarizeQueueOp(entry.op as unknown as Record<string, unknown>) }}</span>
<UiIconButton
icon="i-ph-trash-duotone"
size="sm"
tooltip="Remove from queue"
class="hover-fade"
data-testid="hub-queue-entry-remove"
@click.stop="remove(group.projectId, entry)"
<QueueEntryCard
:entry="entry"
:project-id="group.projectId"
:show-number="true"
@remove="remove(group.projectId, $event)"
/>
</li>
</ul>
Expand Down
4 changes: 3 additions & 1 deletion ui/components/panel/Detail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ async function discardThisItem() {
<div ref="scrollContainer" data-scroll="detail" class="flex-1 overflow-y-auto">
<TabsContent value="conversation">
<PanelDetailConversationTab
v-if="item"
:item="item"
:comments="comments"
:timeline="timeline"
Expand All @@ -427,13 +428,14 @@ async function discardThisItem() {
<PanelDetailPrCommitsTab :commits="commits" />
</TabsContent>
<TabsContent value="changes">
<PanelDetailPrChangesTab :number="item.number" :has-patch="hasPatch" />
<PanelDetailPrChangesTab v-if="item" :number="item.number" :has-patch="hasPatch" />
</TabsContent>
</div>
</TabsRoot>

<div v-else ref="scrollContainer" data-scroll="detail" class="flex-1 overflow-y-auto">
<PanelDetailConversationTab
v-if="item"
:item="item"
:comments="comments"
:timeline="timeline"
Expand Down
29 changes: 4 additions & 25 deletions ui/components/panel/Queue.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ACTIONS_COLOR_HEX } from '#ghfs/action-colors'
import type { QueueEntry } from '#ghfs/server-types'
import type { SyncItemState } from '../../../src/types/sync-state'
import { useActiveProjectId, useAppState } from '../../composables/useAppState'
import { useOnlineState } from '../../composables/useOnlineState'
import { useQueue } from '../../composables/useQueue'
import { useRpc } from '../../composables/useRpc'
import { summarizeQueueOp } from '../../utils/queueSummary'
import DisplayItemStateIcon from '../display/ItemStateIcon.vue'
import QueueEntryCard from '../queue/EntryCard.vue'
import UiEmptyState from '../ui/EmptyState.vue'
import UiIconButton from '../ui/IconButton.vue'
import UiModal from '../ui/Modal.vue'
Expand Down Expand Up @@ -58,14 +57,6 @@ const groups = computed<Group[]>(() => {
return [...byNumber.values()].sort((a, b) => a.number - b.number)
})

function actionColor(action: string): string {
return (ACTIONS_COLOR_HEX as Record<string, string>)[action] ?? '#6b7280'
}

function summarize(entry: QueueEntry): string {
return summarizeQueueOp(entry.op as unknown as Record<string, unknown>)
}

function selectItem(number: number) {
state.selectItem(number)
state.closeQueue()
Expand Down Expand Up @@ -192,27 +183,15 @@ async function confirmExecute() {
</div>
</div>
</button>
<ul class="flex flex-col">
<ul class="flex flex-col gap-2 px-4 py-3 pl-8">
<li
v-for="entry in group.entries"
:key="entry.id"
class="group flex items-start gap-2 px-4 py-1.5 pl-8 hover:bg-active transition"
class="group"
data-testid="queue-entry"
:data-entry-id="entry.id"
>
<span
class="badge font-mono text-xs flex-none"
:style="{ backgroundColor: `${actionColor(entry.op.action)}22`, color: actionColor(entry.op.action) }"
>{{ entry.op.action }}</span>
<span v-if="summarize(entry)" class="text-xs color-muted truncate flex-1">{{ summarize(entry) }}</span>
<span v-else class="flex-1" />
<UiIconButton
icon="i-ph-trash-duotone"
size="sm"
tooltip="Remove from queue"
class="hover-fade"
@click="remove(entry)"
/>
<QueueEntryCard :entry="entry" @remove="remove" />
</li>
</ul>
</section>
Expand Down
Loading
Loading