Skip to content

Commit 5392156

Browse files
feat(downloads): add resume-safe pause action
Give gallery operations distinct pause and cancel paths. Pause preserves partial download data so reinstalling the same model or backend resumes through HTTP Range, while cancel keeps its destructive semantics. Surface the action in the Activity UI and document the API behavior. Assisted-by: Codex:gpt-5
1 parent 1189c68 commit 5392156

20 files changed

Lines changed: 224 additions & 43 deletions

File tree

core/http/react-ui/e2e/activity-page.spec.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,40 @@ test('lists live operations and cancels one from a labelled button', async ({ pa
4343
expect(cancelledPath).toBe('/api/operations/job-gemma/cancel')
4444
})
4545

46+
test('pauses a model download without invoking destructive cancel', async ({ page }) => {
47+
await stub(page, {
48+
operations: [{
49+
id: 'gemma-3-27b-it',
50+
name: 'gemma-3-27b-it',
51+
jobID: 'job-gemma',
52+
progress: 22,
53+
taskType: 'installation',
54+
isBackend: false,
55+
isQueued: false,
56+
isDeletion: false,
57+
cancellable: true,
58+
phase: 'downloading',
59+
}],
60+
})
61+
62+
const requests = []
63+
await page.route('**/api/operations/job-gemma/pause', (route) => {
64+
requests.push(new URL(route.request().url()).pathname)
65+
return route.fulfill({ contentType: 'application/json', body: '{}' })
66+
})
67+
await page.route('**/api/operations/job-gemma/cancel', (route) => {
68+
requests.push(new URL(route.request().url()).pathname)
69+
return route.fulfill({ contentType: 'application/json', body: '{}' })
70+
})
71+
72+
await page.goto('/app/activity')
73+
74+
const card = page.locator('.operation-card').filter({ hasText: 'gemma-3-27b-it' })
75+
await card.locator('.operation-card__pause').click()
76+
77+
await expect.poll(() => requests).toEqual(['/api/operations/job-gemma/pause'])
78+
})
79+
4680
test('separates an unacknowledged failure from the record', async ({ page }) => {
4781
await stub(page, {
4882
operations: [{

core/http/react-ui/public/locales/de/admin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"timeLeft": "{{value}} left",
1313
"cancel": "Cancel",
1414
"cancelLabel": "Cancel {{name}}",
15+
"pause": "Pause",
16+
"pauseLabel": "Pause {{name}} and keep downloaded data",
1517
"retry": "Retry",
1618
"retryLabel": "Retry {{name}}",
1719
"nodeCount": "{{count}} nodes",

core/http/react-ui/public/locales/en/admin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"timeLeft": "{{value}} left",
1313
"cancel": "Cancel",
1414
"cancelLabel": "Cancel {{name}}",
15+
"pause": "Pause",
16+
"pauseLabel": "Pause {{name}} and keep downloaded data",
1517
"retry": "Retry",
1618
"retryLabel": "Retry {{name}}",
1719
"nodeCount": "{{count}} nodes",

core/http/react-ui/public/locales/es/admin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"timeLeft": "{{value}} left",
1313
"cancel": "Cancel",
1414
"cancelLabel": "Cancel {{name}}",
15+
"pause": "Pause",
16+
"pauseLabel": "Pause {{name}} and keep downloaded data",
1517
"retry": "Retry",
1618
"retryLabel": "Retry {{name}}",
1719
"nodeCount": "{{count}} nodes",

core/http/react-ui/public/locales/id/admin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"timeLeft": "{{value}} left",
1313
"cancel": "Cancel",
1414
"cancelLabel": "Cancel {{name}}",
15+
"pause": "Pause",
16+
"pauseLabel": "Pause {{name}} and keep downloaded data",
1517
"retry": "Retry",
1618
"retryLabel": "Retry {{name}}",
1719
"nodeCount": "{{count}} nodes",

core/http/react-ui/public/locales/it/admin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"timeLeft": "{{value}} left",
1313
"cancel": "Cancel",
1414
"cancelLabel": "Cancel {{name}}",
15+
"pause": "Pause",
16+
"pauseLabel": "Pause {{name}} and keep downloaded data",
1517
"retry": "Retry",
1618
"retryLabel": "Retry {{name}}",
1719
"nodeCount": "{{count}} nodes",

core/http/react-ui/public/locales/ko/admin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"timeLeft": "{{value}} left",
1313
"cancel": "Cancel",
1414
"cancelLabel": "Cancel {{name}}",
15+
"pause": "Pause",
16+
"pauseLabel": "Pause {{name}} and keep downloaded data",
1517
"retry": "Retry",
1618
"retryLabel": "Retry {{name}}",
1719
"nodeCount": "{{count}} nodes",

core/http/react-ui/public/locales/zh-CN/admin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"timeLeft": "{{value}} left",
1313
"cancel": "Cancel",
1414
"cancelLabel": "Cancel {{name}}",
15+
"pause": "Pause",
16+
"pauseLabel": "Pause {{name}} and keep downloaded data",
1517
"retry": "Retry",
1618
"retryLabel": "Retry {{name}}",
1719
"nodeCount": "{{count}} nodes",

core/http/react-ui/src/components/OperationCard.jsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function formatEta(seconds) {
2929
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
3030
}
3131

32-
export default function OperationCard({ operation, onCancel, onDismiss, onRetry }) {
32+
export default function OperationCard({ operation, onCancel, onPause, onDismiss, onRetry }) {
3333
const { t } = useTranslation('admin')
3434
const nodes = Array.isArray(operation.nodes) ? operation.nodes : []
3535
// Holds only what the user chose. The default has to stay a live
@@ -144,6 +144,16 @@ export default function OperationCard({ operation, onCancel, onDismiss, onRetry
144144

145145
<div className="operation-card__actions">
146146
{showProgress && <span className="operation-card__pct" aria-hidden="true">{Math.round(operation.progress)}%</span>}
147+
{canCancel && (
148+
<button
149+
type="button"
150+
className="btn btn-sm btn-secondary operation-card__pause"
151+
onClick={() => onPause?.(operation.jobID)}
152+
aria-label={t('activity.pauseLabel', { name })}
153+
>
154+
{t('activity.pause')}
155+
</button>
156+
)}
147157
{canCancel && (
148158
// A page of cards would otherwise hand a screen reader a list of
149159
// identical "Cancel" buttons with nothing to tell them apart.

core/http/react-ui/src/contexts/OperationsContext.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ export function OperationsProvider({ children, pollInterval = 1000 }) {
137137
}
138138
}, [fetchOperations])
139139

140+
const pauseOperation = useCallback(async (jobID) => {
141+
try {
142+
await operationsApi.pause(jobID)
143+
cancelledRef.current.set(jobID, Date.now())
144+
await fetchOperations()
145+
} catch (err) {
146+
setError(err.message)
147+
}
148+
}, [fetchOperations])
149+
140150
// Whether this tab cancelled the job. Read by the strip to tell "the last
141151
// operation finished" from "the user called it off": both look identical in
142152
// /api/operations, which lists neither.
@@ -226,6 +236,7 @@ export function OperationsProvider({ children, pollInterval = 1000 }) {
226236
fetchHistory,
227237
clearHistory,
228238
cancelOperation,
239+
pauseOperation,
229240
wasCancelled,
230241
dismissFailedOp,
231242
refetch: fetchOperations,

0 commit comments

Comments
 (0)