diff --git a/desktop/e2e/tests/actions.spec.ts b/desktop/e2e/tests/actions.spec.ts index 4cb9c259..5dead7de 100644 --- a/desktop/e2e/tests/actions.spec.ts +++ b/desktop/e2e/tests/actions.spec.ts @@ -40,9 +40,9 @@ actions: applies_to: [pr] repo_template: "https://github.com/{{ .Payload.repo }}.git" prompt_template: | - Review pull request {{ .Payload.url }}: {{ .Payload.title }} + Review pull request {{ .Payload.title }} - {{ .Payload.body }} + {{ .Payload.url }} - id: start-implementation label: Start implementation type: launch-session @@ -50,7 +50,9 @@ actions: applies_to: [issue] repo_template: "https://github.com/{{ .Payload.repo }}.git" prompt_template: | - Implement issue {{ .Payload.url }}: {{ .Payload.title }} + Work on {{ .Payload.title }} + + {{ .Payload.url }} {{ .Payload.body }} `) @@ -117,6 +119,12 @@ test('persists shell output, failure diagnostics, and durable duplicate rejectio const shell = action(state.runId, 'pr') await page.locator(`[data-id="${shell}"]`).click() await expect(page.getByTestId('toast')).toContainText('Smoke PR completed') + const jobsChip = page.getByTestId('titlebar-jobs') + await expect(jobsChip).toBeVisible() + await jobsChip.click() + await expect(page.getByTestId('jobs-popover')).toContainText('Smoke PR') + await expect(page.getByTestId('jobs-popover')).toContainText('Completed') + await expect(jobsChip).toBeHidden({ timeout: 7_000 }) await expect.poll(async () => (await smoke(page)).outputCommands.filter((command) => command.actionId === shell)).toEqual([ expect.objectContaining({ key: 'pr2841', status: 'done', stdout: 'smoke-stdout', stderr: 'smoke-stderr', lastError: '' }), ]) diff --git a/desktop/frontend/bindings/github.com/colonyops/hive/desktop/index.ts b/desktop/frontend/bindings/github.com/colonyops/hive/desktop/index.ts index 175893e0..c9af125a 100644 --- a/desktop/frontend/bindings/github.com/colonyops/hive/desktop/index.ts +++ b/desktop/frontend/bindings/github.com/colonyops/hive/desktop/index.ts @@ -4,6 +4,7 @@ import * as ActionsService from "./actionsservice.js"; import * as ActivityService from "./activityservice.js"; import * as FlowsService from "./flowsservice.js"; +import * as JobService from "./jobservice.js"; import * as PipelineService from "./pipelineservice.js"; import * as SettingsService from "./settingsservice.js"; import * as SystemService from "./systemservice.js"; @@ -11,6 +12,7 @@ export { ActionsService, ActivityService, FlowsService, + JobService, PipelineService, SettingsService, SystemService diff --git a/desktop/frontend/bindings/github.com/colonyops/hive/desktop/jobservice.ts b/desktop/frontend/bindings/github.com/colonyops/hive/desktop/jobservice.ts new file mode 100644 index 00000000..3da42b0f --- /dev/null +++ b/desktop/frontend/bindings/github.com/colonyops/hive/desktop/jobservice.ts @@ -0,0 +1,32 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * JobService exposes live action-run jobs to the desktop frontend. The + * titlebar reads active and briefly lingering jobs through ListActive, while + * List provides cursor-based history paging. + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as jobs$0 from "../internal/desktop/jobs/models.js"; + +/** + * List returns up to limit jobs with id < before, newest first. + */ +export function List(before: number, limit: number): $CancellablePromise { + return $Call.ByID(1946050826, before, limit); +} + +/** + * ListActive returns non-terminal jobs plus terminal jobs completed within the + * backend-owned lingering window. + */ +export function ListActive(): $CancellablePromise { + return $Call.ByID(2667553212); +} diff --git a/desktop/frontend/bindings/github.com/colonyops/hive/internal/desktop/jobs/index.ts b/desktop/frontend/bindings/github.com/colonyops/hive/internal/desktop/jobs/index.ts new file mode 100644 index 00000000..82e645a8 --- /dev/null +++ b/desktop/frontend/bindings/github.com/colonyops/hive/internal/desktop/jobs/index.ts @@ -0,0 +1,7 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export type { + Job, + JobStatus +} from "./models.js"; diff --git a/desktop/frontend/bindings/github.com/colonyops/hive/internal/desktop/jobs/models.ts b/desktop/frontend/bindings/github.com/colonyops/hive/internal/desktop/jobs/models.ts new file mode 100644 index 00000000..9a27f23c --- /dev/null +++ b/desktop/frontend/bindings/github.com/colonyops/hive/internal/desktop/jobs/models.ts @@ -0,0 +1,27 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * Job is one action-run lifecycle row. ID, CreatedAt, and UpdatedAt are + * assigned by the store. CreatedAt and UpdatedAt are unix milliseconds. Step + * is a human label derived from Status in v1. + */ +export interface Job { + "id": number; + "createdAt": number; + "updatedAt": number; + "status": JobStatus; + "label": string; + "step": string; + "actionId": string; + "target": string; + "error"?: string; + "commandId"?: number | null; +} + +/** + * JobStatus is a job's lifecycle stage. + * + * ENUM(queued, running, done, failed) + */ +export type JobStatus = string; diff --git a/desktop/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/desktop/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts index 63d0f03f..7a04e21a 100644 --- a/desktop/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts +++ b/desktop/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -12,6 +12,7 @@ declare module "@wailsio/runtime" { "activity:appended": number; "auth:updated": string; "flows:updated": string; + "jobs:updated": string; "log:appended": number; } } diff --git a/desktop/frontend/src/App.vue b/desktop/frontend/src/App.vue index 2e2efd67..0d2bb5a1 100644 --- a/desktop/frontend/src/App.vue +++ b/desktop/frontend/src/App.vue @@ -28,6 +28,7 @@ import ToastStack from './components/ToastStack.vue' import DevBar from './components/DevBar.vue' import { useAuth } from './composables/useAuth' import { useActivity } from './composables/useActivity' +import { useJobs } from './composables/useJobs' import { useFeedState } from './composables/useFeedState' import { useCommands, useCommandPalette, type Command } from './composables/useCommands' import { comboFromEvent, formatCombo, useKeybindings } from './composables/useKeybindings' @@ -51,7 +52,7 @@ const { profiles, profilesLoaded, profilesError, activeProfile, activeProfileId, selection, items, visibleItems, unreadCount, search, loadError, selectedId, selectedItem, actions, pendingAction, actionRuns, sessionLaunchAction, sessionLaunchOptions, sessionLaunchBusy, sessionLaunchError, unreadOnly, feedSort, setFeedSort, title, toasts, dismissToast, clearToasts, creatingProfile, createProfileError, renamingProfile, renameProfileError, deletingProfile, loadProfiles, createProfile, renameProfile, deleteProfile, - reorderFeeds, selectProfile, selectSidebar, selectUnreadView, selectItem, selectNext, selectPrev, + reorderFeeds, selectProfile, selectSidebar, selectUnreadView, selectItem, openActionRun, selectNext, selectPrev, toggleUnread, refresh, invokeAction, cancelSessionLaunch, submitSessionLaunch, notWired, openUrl, openSelectedInBrowser, hideWindow, } = useFeedState() @@ -256,11 +257,21 @@ function requestOpenSettings(page: 'application' | 'profile'): void { // App-global audit log. The titlebar's Activity link replaces the old "polling // github" indicator; unseenActivity drives its dot. const { unseenCount: unseenActivity } = useActivity() +const { activeJobs, hasActive: jobsActive } = useJobs() function openActivity(): void { void router.push({ name: 'activity' }) } +async function openJobRun(commandID: number): Promise { + const job = activeJobs.value.find((candidate) => candidate.commandId === commandID) + if (!job || !activeProfileId.value) return + await router.push({ name: 'feed', params: { profileId: activeProfileId.value } }) + if (route.name !== 'feed') return + await selectSidebar({ type: 'all' }) + await openActionRun(job.target, job.actionId, commandID) +} + function closeSettings(): void { openFeed() } @@ -586,6 +597,8 @@ onUnmounted(() => { :activity-active="activityActive" :error-count="errorCount" :unseen-activity="unseenActivity" + :jobs-active="jobsActive" + :active-jobs="activeJobs" :can-go-back="canGoBack" :can-go-forward="canGoForward" :sidebar-collapsed="sidebarCollapsed" @@ -594,6 +607,7 @@ onUnmounted(() => { @forward="router.forward()" @open-error-node="openErrorNode" @open-activity="openActivity" + @open-job-run="openJobRun" @toggle-sidebar="toggleSidebar" @open-palette="togglePalette" @toggle-maximise="toggleMaximise" diff --git a/desktop/frontend/src/components/JobsPopover.vue b/desktop/frontend/src/components/JobsPopover.vue new file mode 100644 index 00000000..50b8ab05 --- /dev/null +++ b/desktop/frontend/src/components/JobsPopover.vue @@ -0,0 +1,59 @@ + + + diff --git a/desktop/frontend/src/components/TitleBar.vue b/desktop/frontend/src/components/TitleBar.vue index 002a83e6..25f70616 100644 --- a/desktop/frontend/src/components/TitleBar.vue +++ b/desktop/frontend/src/components/TitleBar.vue @@ -1,4 +1,6 @@