fix(knowledge-fs): show task object titles

This commit is contained in:
Jyong 2026-09-02 01:43:16 -04:00
parent 222d09f474
commit 049a4c514e
24 changed files with 387 additions and 22 deletions

View File

@ -1,7 +1,7 @@
{
"schemaVersion": 5,
"subtreeTree": "4c8a98b2c31329e0dbaf7c5e05233afc05f2611b",
"openapiSha256": "e147709323f72bfb2296b03ee4535eedb3365bbd232847faef49467ea12a5416",
"subtreeTree": "b28f85a3efed562af70e5061cf5a7552394cd908",
"openapiSha256": "1214898a307c01993690cf239b0989be748cbf9b91437670f270220223407e06",
"capabilityV2AuthManifestSha256": "e322a2fa779d1f40b95c54c1021cffecaec77abbd7b34899573dcdf4ff353109",
"capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3",
"productOperationManifestSha256": "5d1241a83bcca12ebbd848928dd3cdda0d2ecaeb5f5336e955eb24a8c8db175b",

View File

@ -2066,6 +2066,7 @@ class KnowledgeFSBackgroundTaskResponse(ResponseModel):
progress_percent: int = Field(ge=0, le=100, validation_alias=AliasChoices("progress_percent", "progressPercent"))
progress_total: int = Field(ge=0, validation_alias=AliasChoices("progress_total", "progressTotal"))
source_id: str | None = Field(default=None, validation_alias=AliasChoices("source_id", "sourceId"))
source_title: str | None = Field(default=None, validation_alias=AliasChoices("source_title", "sourceTitle"))
state: Literal["canceled", "completed", "failed", "queued", "running"]
task_kind: Literal["document", "document_bulk", "source"] = Field(
validation_alias=AliasChoices("task_kind", "taskKind")

View File

@ -8,6 +8,7 @@ from pydantic import ValidationError
from services.knowledge_fs.product_dto import (
KnowledgeFSBackgroundTaskListQuery,
KnowledgeFSBackgroundTaskListResponse,
KnowledgeFSBackgroundTaskResponse,
KnowledgeFSBadCaseCreatePayload,
KnowledgeFSBadCaseUpdatePayload,
KnowledgeFSBulkJobResponse,
@ -70,6 +71,30 @@ from services.knowledge_fs.product_dto import (
)
def test_background_task_response_accepts_source_title_alias() -> None:
task = KnowledgeFSBackgroundTaskResponse.model_validate(
{
"canCancel": False,
"canRetry": False,
"createdAt": "2026-09-02T12:00:00Z",
"id": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c50",
"knowledgeSpaceId": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c40",
"operation": "source_sync",
"progressCompleted": 1,
"progressFailed": 0,
"progressPercent": 100,
"progressTotal": 1,
"sourceId": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c60",
"sourceTitle": "Notion support SOP",
"state": "completed",
"taskKind": "source",
"updatedAt": "2026-09-02T12:01:00Z",
}
)
assert task.source_title == "Notion support SOP"
def test_quality_replay_payload_requires_exactly_one_selection_mode() -> None:
assert KnowledgeFSQualityReplayPayload(selection="all-active").selection == "all-active"
assert KnowledgeFSQualityReplayPayload(golden_question_ids=["question-1"]).golden_question_ids == ["question-1"]

View File

@ -0,0 +1,30 @@
# Background Task Object Titles
## Problem
The Task drawer displayed a single-document re-index as `Re-index · 1` because progress count
took precedence over the document title. Source sync rows depended on the Source list page already
being loaded; otherwise their one-item progress count was also presented as the object name.
## Changes
- Persist the logical-document title or asset filename in newly created re-index bulk-operation
items, so a single re-index has a durable display title.
- Add an optional `sourceTitle` to the background-task contract. The task handler resolves all
selected Source task names in one bounded, space-scoped repository query, rechecks content
grants, and treats title lookup as non-critical enrichment.
- Return the same Source title from cancel and retry responses. Deleting, unavailable, or
unauthorized Sources never disclose a name and do not make task listing unavailable.
- Prefer object names in both task drawer implementations. A true multi-document re-index may
still show its item count, while a single task without a resolvable name no longer labels itself
as `1`.
## Verification
- Focused KnowledgeFS background-task, Source repository, and re-index persistence tests cover
PostgreSQL and TiDB placeholders, one-query enrichment, permission filtering, enrichment
degradation, and durable filename capture.
- Focused document-list and document-detail UI tests cover single re-index filenames and Source
task-provided names when the Source list is not loaded.
- The Dify DTO test covers camel-case `sourceTitle` validation and the generated TypeScript API
contract.

View File

@ -1,4 +1,4 @@
import type { AuthSubject } from "@knowledge/core";
import type { AuthSubject, Source } from "@knowledge/core";
import { describe, expect, it, vi } from "vitest";
import {
@ -72,18 +72,24 @@ describe("background task handlers", () => {
const listRecentRuns = vi.fn(async () => ({
items: [sourceRun({ createdAt: "2026-07-23T12:04:00.000Z" })],
}));
const getMany = vi.fn(async () => [sourceRecord()]);
const app = backgroundTaskApp({
bulkOperations,
compilationJobs,
documentTasks,
sourceRepository: { listRecentRuns },
sources: { getMany },
});
const response = await app.request(`/knowledge-spaces/${SPACE_ID}/background-tasks?limit=2`);
expect(response.status).toBe(200);
const body = await response.json();
expect(body.items).toEqual([
expect.objectContaining({ id: SOURCE_RUN_ID, taskKind: "source" }),
expect.objectContaining({
id: SOURCE_RUN_ID,
sourceTitle: "Notion support SOP",
taskKind: "source",
}),
expect.objectContaining({ id: BULK_ID, taskKind: "document_bulk" }),
]);
expect(body.nextCursor).toEqual(expect.any(String));
@ -98,6 +104,42 @@ describe("background task handlers", () => {
expect(listRecentRuns).toHaveBeenCalledWith(
expect.objectContaining({ candidateGrants: ["scope:visible"], limit: 2 }),
);
expect(getMany).toHaveBeenCalledOnce();
expect(getMany).toHaveBeenCalledWith({ ids: [SOURCE_ID], knowledgeSpaceId: SPACE_ID });
});
it("does not disclose a source title outside the current content grants", async () => {
const app = backgroundTaskApp({
sourceRepository: { listRecentRuns: vi.fn(async () => ({ items: [sourceRun()] })) },
sources: {
getMany: vi.fn(async () => [sourceRecord({ permissionScope: ["scope:hidden"] })]),
},
});
const response = await app.request(`/knowledge-spaces/${SPACE_ID}/background-tasks?limit=10`);
expect(response.status).toBe(200);
const body = await response.json();
expect(body.items).toEqual([
expect.objectContaining({ id: SOURCE_RUN_ID, sourceId: SOURCE_ID }),
]);
expect(body.items[0]).not.toHaveProperty("sourceTitle");
});
it("keeps task listing available when source title enrichment is unavailable", async () => {
const app = backgroundTaskApp({
sourceRepository: { listRecentRuns: vi.fn(async () => ({ items: [sourceRun()] })) },
sources: {
getMany: vi.fn(async () => {
throw new Error("source catalog unavailable");
}),
},
});
const response = await app.request(`/knowledge-spaces/${SPACE_ID}/background-tasks?limit=10`);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
items: [expect.objectContaining({ id: SOURCE_RUN_ID, sourceId: SOURCE_ID })],
});
});
it("does not re-emit a grouped document after its bulk task leaves the page", async () => {
@ -288,6 +330,7 @@ describe("background task handlers", () => {
);
const retry = vi.fn(async () => sourceRun({ state: "queued" }));
const app = backgroundTaskApp({
sources: { getMany: vi.fn(async () => [sourceRecord()]) },
sourceWorkflows: { cancel, retry },
});
@ -298,6 +341,7 @@ describe("background task handlers", () => {
expect(canceled.status).toBe(200);
await expect(canceled.json()).resolves.toMatchObject({
canRetry: true,
sourceTitle: "Notion support SOP",
state: "canceled",
taskKind: "source",
});
@ -306,7 +350,10 @@ describe("background task handlers", () => {
{ method: "POST" },
);
expect(retried.status).toBe(200);
await expect(retried.json()).resolves.toMatchObject({ state: "queued" });
await expect(retried.json()).resolves.toMatchObject({
sourceTitle: "Notion support SOP",
state: "queued",
});
expect(cancel).toHaveBeenCalledWith(
expect.objectContaining({ knowledgeSpaceId: SPACE_ID, runId: SOURCE_RUN_ID }),
);
@ -730,6 +777,7 @@ function backgroundTaskApp(overrides: {
readonly durableDeletions?: RegisterBackgroundTaskHandlersOptions["durableDeletions"];
readonly space?: { readonly id: string } | null;
readonly sourceRepository?: object;
readonly sources?: object;
readonly sourceWorkflows?: object;
}) {
const app = createKnowledgeGatewayApp();
@ -763,6 +811,7 @@ function backgroundTaskApp(overrides: {
...(overrides.sourceRepository
? { sourceRepository: overrides.sourceRepository as never }
: {}),
...(overrides.sources ? { sources: overrides.sources as never } : {}),
...(overrides.sourceWorkflows ? { sourceWorkflows: overrides.sourceWorkflows as never } : {}),
spaces: {
get: vi.fn(async ({ id, tenantId }) => {
@ -892,6 +941,23 @@ function sourceRun(patch: Partial<SourceWorkflowRun> = {}): SourceWorkflowRun {
};
}
function sourceRecord(patch: Partial<Source> = {}): Source {
return {
createdAt: "2026-07-23T12:00:00.000Z",
id: SOURCE_ID,
knowledgeSpaceId: SPACE_ID,
metadata: {},
name: "Notion support SOP",
permissionScope: ["scope:visible"],
status: "active",
type: "web",
updatedAt: "2026-07-23T12:01:00.000Z",
uri: "https://example.com",
version: 1,
...patch,
};
}
function authorizationDecision() {
return {
accessContext: {} as never,

View File

@ -16,7 +16,10 @@ import {
} from "./background-task-routes";
import { type BulkOperationRepository, canReadBulkOperation } from "./bulk-operation";
import { summarizeBulkOperation } from "./bulk-operation-summary";
import { currentCandidateGrants } from "./candidate-content-authorization";
import {
candidatePermissionScopeAllows,
currentCandidateGrants,
} from "./candidate-content-authorization";
import { issueKnowledgeSpaceDurablePermission } from "./derived-result-authorization";
import type {
DocumentCompilationJobStateMachine,
@ -40,6 +43,7 @@ import type {
SourceProductWorkflowService,
SourceWorkflowPrincipal,
} from "./source-product-workflow";
import type { SourceRepository } from "./source-repository";
type CandidateSource = "bulk" | "document" | "source";
@ -59,6 +63,7 @@ export interface RegisterBackgroundTaskHandlersOptions {
readonly durableDeletionJobs?: Pick<DurableDeletionRepository, "getJob"> | undefined;
readonly durableDeletions?: DurableDeletionService | undefined;
readonly sourceRepository?: SourceProductWorkflowRepository | undefined;
readonly sources?: Pick<SourceRepository, "getMany"> | undefined;
readonly sourceWorkflows?: SourceProductWorkflowService | undefined;
readonly spaces: KnowledgeSpaceRepository;
}
@ -73,6 +78,7 @@ export function registerBackgroundTaskHandlers({
durableDeletionJobs,
durableDeletions,
sourceRepository,
sources,
sourceWorkflows,
spaces,
}: RegisterBackgroundTaskHandlersOptions): void {
@ -176,6 +182,12 @@ export function registerBackgroundTaskHandlers({
compareCandidates,
);
const selected = candidates.slice(0, query.limit);
const selectedTasks = await attachSourceTitles(
selected.map((candidate) => candidate.task),
sources,
grants,
params.id,
);
let next = advanceCursor(cursor, selected);
if (documentCandidates.length === 0 && documentPage.items.length > 0) {
const last = documentPage.items.at(-1);
@ -191,7 +203,7 @@ export function registerBackgroundTaskHandlers({
Boolean(documentPage.nextCursor || bulkPage.nextCursor || sourcePage.nextCursor);
return context.json(
{
items: selected.map((candidate) => candidate.task),
items: selectedTasks,
...(hasMore ? { nextCursor: encodeBackgroundTaskCursor(next) } : {}),
},
200,
@ -247,7 +259,9 @@ export function registerBackgroundTaskHandlers({
: await controlSourceTask({
action,
context,
grants,
knowledgeSpaceId: params.id,
sources,
sourceWorkflows,
taskId: params.taskId,
});
@ -444,7 +458,9 @@ async function controlSourceTask(input: {
readonly action: "cancel" | "retry";
// biome-ignore lint/suspicious/noExplicitAny: bounded OpenAPI handler context
readonly context: any;
readonly grants: readonly string[];
readonly knowledgeSpaceId: string;
readonly sources?: Pick<SourceRepository, "getMany"> | undefined;
readonly sourceWorkflows?: SourceProductWorkflowService | undefined;
readonly taskId: string;
}): Promise<BackgroundTask | null> {
@ -463,7 +479,44 @@ async function controlSourceTask(input: {
knowledgeSpaceId: input.knowledgeSpaceId,
runId: input.taskId,
});
return run ? sourceBackgroundTask(run) : null;
if (!run) return null;
const task = sourceBackgroundTask(run);
return (
(await attachSourceTitles([task], input.sources, input.grants, input.knowledgeSpaceId))[0] ??
task
);
}
async function attachSourceTitles(
tasks: readonly BackgroundTask[],
sources: Pick<SourceRepository, "getMany"> | undefined,
candidateGrants: readonly string[],
knowledgeSpaceId: string,
): Promise<BackgroundTask[]> {
if (!sources) return [...tasks];
const sourceIds = [...new Set(tasks.flatMap((task) => (task.sourceId ? [task.sourceId] : [])))];
if (sourceIds.length === 0) return [...tasks];
try {
const visibleSourceTitles = new Map(
(
await sources.getMany({
ids: sourceIds,
knowledgeSpaceId,
})
)
.filter((source) => candidatePermissionScopeAllows(source.permissionScope, candidateGrants))
.map((source) => [source.id, source.name] as const),
);
return tasks.map((task) => {
const sourceTitle = task.sourceId ? visibleSourceTitles.get(task.sourceId) : undefined;
return sourceTitle ? { ...task, sourceTitle } : task;
});
} catch {
// A title is display enrichment only; task control and visibility must remain available if the
// source catalog is temporarily unavailable.
return [...tasks];
}
}
async function controlPermission(

View File

@ -80,6 +80,7 @@ export const BackgroundTaskSchema = z.object({
progressPercent: z.number().int().min(0).max(100),
progressTotal: z.number().int().nonnegative(),
sourceId: z.string().uuid().optional(),
sourceTitle: z.string().min(1).optional(),
state: BackgroundTaskStateSchema,
semanticEnrichment: z
.object({

View File

@ -55,6 +55,7 @@ export interface BackgroundTask {
readonly progressPercent: number;
readonly progressTotal: number;
readonly sourceId?: string | undefined;
readonly sourceTitle?: string | undefined;
readonly state: BackgroundTaskState;
readonly semanticEnrichment?: DocumentSemanticEnrichmentProgress | undefined;
readonly taskKind: BackgroundTaskKind;

View File

@ -440,6 +440,7 @@ export function registerDocumentWriteHandlers({
bulkItems.push({
compilationJobId: compilationJob.id,
documentId: logicalDocument?.id ?? asset.id,
documentTitle: logicalDocument?.title ?? asset.filename,
requiredPermissionScope: requiredPermissionScopeForAsset(asset),
status: "queued",
});

View File

@ -3627,9 +3627,14 @@ describe("document write gateway integration", () => {
now: () => 1_777_777_000_000,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 10 }),
});
const bulkOperations = createInMemoryBulkOperationRepository({
maxItems: 10,
maxOperations: 10,
});
const app = createKnowledgeGateway({
adapter,
auth: createTestAuthVerifier(),
bulkOperations,
documentAssets: assets,
documentCompilationJobs: compilationJobs,
generateBulkUploadId: () => "bulk-reindex-1",
@ -3692,6 +3697,14 @@ describe("document write gateway integration", () => {
],
total: 2,
});
const storedOperation = await bulkOperations.get({
id: "bulk-reindex-1",
tenantId: "tenant-1",
});
expect(storedOperation?.items[0]).toMatchObject({
documentId: first.id,
documentTitle: "First.md",
});
const all = await app.request(
"/knowledge-spaces/018f0d60-7a49-7cc2-9c1b-5b36f18f2c42/documents/bulk/reindex",

View File

@ -2219,6 +2219,7 @@ export function createKnowledgeGateway({
...(durableDeletionService ? { durableDeletions: durableDeletionService } : {}),
...(sourceProduct ? { sourceRepository: sourceProduct.repository } : {}),
...(sourceProductWorkflows ? { sourceWorkflows: sourceProductWorkflows } : {}),
sources: sourceRepository,
spaces,
});
registerSourceHandlers({

View File

@ -54,6 +54,12 @@ describe("createInMemorySourceRepository", () => {
await expect(
repository.get({ id: created.id, knowledgeSpaceId: SPACE_A }),
).resolves.toMatchObject({ id: created.id });
await expect(
repository.getMany({ ids: [created.id, "missing", created.id], knowledgeSpaceId: SPACE_A }),
).resolves.toEqual([expect.objectContaining({ id: created.id, name: "Docs crawl" })]);
await expect(
repository.getMany({ ids: [created.id], knowledgeSpaceId: SPACE_B }),
).resolves.toEqual([]);
const updated = await repository.update({
id: created.id,
@ -190,6 +196,50 @@ describe("createDatabaseSourceRepository", () => {
expect(calls[1]?.sql).not.toContain("<> 'deleting'");
});
it.each(["postgres", "tidb"] as const)(
"batch-loads active sources in one space-scoped query for %s",
async (dialect) => {
const calls: DatabaseExecuteInput[] = [];
const firstId = "00000000-0000-4000-8000-000000000001";
const secondId = "00000000-0000-4000-8000-000000000002";
const repository = createDatabaseSourceRepository({
database: createSchemaDatabaseAdapter({
executor: async (input) => {
calls.push(input);
return {
rows: [
sourceRow(firstId, { name: "First source" }),
sourceRow(secondId, { name: "Second source" }),
],
rowsAffected: 2,
};
},
kind: dialect,
}),
});
await expect(
repository.getMany({
ids: [secondId, firstId, secondId],
knowledgeSpaceId: SPACE_A,
}),
).resolves.toEqual([
expect.objectContaining({ id: firstId, name: "First source" }),
expect.objectContaining({ id: secondId, name: "Second source" }),
]);
expect(calls).toHaveLength(1);
expect(calls[0]?.params).toEqual([SPACE_A, firstId, secondId]);
expect(calls[0]?.sql).toContain("status");
expect(calls[0]?.sql).toContain("<> 'deleting'");
expect(calls[0]?.sql).toContain("ORDER BY");
if (dialect === "tidb") {
const call = calls[0];
if (!call) throw new Error("Expected a batch source lookup query");
expect(call.sql.match(/\?/g)).toHaveLength(call.params.length);
}
},
);
it.each(["postgres", "tidb"] as const)(
"maps the lifecycle-only deleting status through getForDeletion for %s",
async (dialect) => {

View File

@ -41,6 +41,11 @@ export interface SourceLookupInput {
readonly knowledgeSpaceId: string;
}
export interface SourceBatchLookupInput {
readonly ids: readonly string[];
readonly knowledgeSpaceId: string;
}
/** Internal row shape used only while a durable deletion is fenced. */
export type SourceForDeletion = Omit<Source, "status"> & {
readonly status: Source["status"] | "deleting";
@ -136,6 +141,8 @@ export interface SourceRepository {
): Promise<Source | null>;
create(input: CreateSourceInput): Promise<Source>;
get(input: SourceLookupInput): Promise<Source | null>;
/** Bounded, space-scoped lookup used to enrich task lists without per-row queries. */
getMany(input: SourceBatchLookupInput): Promise<Source[]>;
/** Internal durable-deletion lookup; includes a row already fenced as deleting. */
getForDeletion(input: SourceLookupInput): Promise<SourceForDeletion | null>;
list(input: ListSourcesInput): Promise<ListSourcesResult>;
@ -246,6 +253,13 @@ export function createInMemorySourceRepository({
return source && source.knowledgeSpaceId === knowledgeSpaceId ? cloneSource(source) : null;
},
getMany: async ({ ids, knowledgeSpaceId }) => {
const requestedIds = normalizeSourceBatchLookupIds(ids);
return requestedIds.flatMap((id) => {
const source = sources.get(id);
return source?.knowledgeSpaceId === knowledgeSpaceId ? [cloneSource(source)] : [];
});
},
getForDeletion: async ({ id, knowledgeSpaceId }) => {
const source = sources.get(id);
@ -517,6 +531,30 @@ export function createDatabaseSourceRepository({
return result.rows[0] ? mapDatabaseSourceRow(result.rows[0]) : source;
},
get: async (input) => databaseSourceGet(database, input),
getMany: async ({ ids, knowledgeSpaceId }) => {
const requestedIds = normalizeSourceBatchLookupIds(ids);
if (requestedIds.length === 0) return [];
const params = [knowledgeSpaceId, ...requestedIds] satisfies readonly DatabaseQueryValue[];
const result = await database.execute({
maxRows: requestedIds.length,
operation: "select",
params,
sql: `SELECT * FROM ${quoteDatabaseIdentifier(database, tableName)} WHERE ${quoteDatabaseIdentifier(
database,
"knowledge_space_id",
)} = ${databasePlaceholder(database, 1)} AND ${quoteDatabaseIdentifier(
database,
"id",
)} IN (${requestedIds
.map((_, index) => databasePlaceholder(database, index + 2))
.join(", ")}) AND ${quoteDatabaseIdentifier(
database,
"status",
)} <> 'deleting' ORDER BY ${quoteDatabaseIdentifier(database, "id")} ASC;`,
tableName,
});
return result.rows.map(mapDatabaseSourceRow).map(cloneSource);
},
getForDeletion: async (input) => databaseSourceGetForDeletion(database, input),
list: async ({ cursor, knowledgeSpaceId, limit }) => {
validateSourceListLimit(limit);
@ -790,3 +828,11 @@ function validateSourceListLimit(limit: number): void {
throw new Error("Source list limit must be at least 1");
}
}
function normalizeSourceBatchLookupIds(ids: readonly string[]): string[] {
const uniqueIds = [...new Set(ids)].sort((left, right) => left.localeCompare(right));
if (uniqueIds.length > 100) {
throw new Error("Source batch lookup exceeds 100 ids");
}
return uniqueIds;
}

View File

@ -174,6 +174,7 @@ export type KnowledgeFsBackgroundTaskResponse = {
progress_percent: number
progress_total: number
source_id?: string | null
source_title?: string | null
state: 'canceled' | 'completed' | 'failed' | 'queued' | 'running'
task_kind: 'document' | 'document_bulk' | 'source'
updated_at: string
@ -866,6 +867,8 @@ export type KnowledgeFsSourceWorkflowResponse = {
| 'RESEARCH_TASK_FAILED'
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
| 'RETRIEVAL_DELETION_IN_PROGRESS'
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
| 'SOURCE_BULK_ACTION_FAILED'
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
@ -886,6 +889,7 @@ export type KnowledgeFsSourceWorkflowResponse = {
| 'SOURCE_SECRET_INTEGRITY_FAILED'
| 'SOURCE_SECRET_REF_CONFLICT'
| 'SOURCE_SYNC_FAILED'
| 'SOURCE_SYNC_SELECTION_MISMATCH'
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
| 'SOURCE_WEBSITE_CRAWL_FAILED'
| 'SOURCE_WORKFLOW_FAILED'
@ -1093,6 +1097,8 @@ export type KnowledgeFsSourceCredentialTestResponse = {
| 'RESEARCH_TASK_FAILED'
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
| 'RETRIEVAL_DELETION_IN_PROGRESS'
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
| 'SOURCE_BULK_ACTION_FAILED'
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
@ -1113,6 +1119,7 @@ export type KnowledgeFsSourceCredentialTestResponse = {
| 'SOURCE_SECRET_INTEGRITY_FAILED'
| 'SOURCE_SECRET_REF_CONFLICT'
| 'SOURCE_SYNC_FAILED'
| 'SOURCE_SYNC_SELECTION_MISMATCH'
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
| 'SOURCE_WEBSITE_CRAWL_FAILED'
| 'SOURCE_WORKFLOW_FAILED'
@ -1449,6 +1456,8 @@ export type KnowledgeFsPublicFailureResponse = {
| 'RESEARCH_TASK_FAILED'
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
| 'RETRIEVAL_DELETION_IN_PROGRESS'
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
| 'SOURCE_BULK_ACTION_FAILED'
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
@ -1469,6 +1478,7 @@ export type KnowledgeFsPublicFailureResponse = {
| 'SOURCE_SECRET_INTEGRITY_FAILED'
| 'SOURCE_SECRET_REF_CONFLICT'
| 'SOURCE_SYNC_FAILED'
| 'SOURCE_SYNC_SELECTION_MISMATCH'
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
| 'SOURCE_WEBSITE_CRAWL_FAILED'
| 'SOURCE_WORKFLOW_FAILED'
@ -1985,6 +1995,8 @@ export type KnowledgeFsSourceImportFailureResponse = {
| 'RESEARCH_TASK_FAILED'
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
| 'RETRIEVAL_DELETION_IN_PROGRESS'
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
| 'SOURCE_BULK_ACTION_FAILED'
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
@ -2005,6 +2017,7 @@ export type KnowledgeFsSourceImportFailureResponse = {
| 'SOURCE_SECRET_INTEGRITY_FAILED'
| 'SOURCE_SECRET_REF_CONFLICT'
| 'SOURCE_SYNC_FAILED'
| 'SOURCE_SYNC_SELECTION_MISMATCH'
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
| 'SOURCE_WEBSITE_CRAWL_FAILED'
| 'SOURCE_WORKFLOW_FAILED'

View File

@ -952,6 +952,8 @@ export const zKnowledgeFsPublicFailureResponse = z.object({
'RESEARCH_TASK_FAILED',
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
'RETRIEVAL_DELETION_IN_PROGRESS',
'RETRIEVAL_EXECUTION_LEASE_LOST',
'SOURCE_BULK_ACTION_FAILED',
'SOURCE_CREDENTIAL_CONFIG_INVALID',
'SOURCE_CREDENTIAL_MUTATION_FAILED',
@ -972,6 +974,7 @@ export const zKnowledgeFsPublicFailureResponse = z.object({
'SOURCE_SECRET_INTEGRITY_FAILED',
'SOURCE_SECRET_REF_CONFLICT',
'SOURCE_SYNC_FAILED',
'SOURCE_SYNC_SELECTION_MISMATCH',
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
'SOURCE_WEBSITE_CRAWL_FAILED',
'SOURCE_WORKFLOW_FAILED',
@ -1055,6 +1058,8 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({
'RESEARCH_TASK_FAILED',
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
'RETRIEVAL_DELETION_IN_PROGRESS',
'RETRIEVAL_EXECUTION_LEASE_LOST',
'SOURCE_BULK_ACTION_FAILED',
'SOURCE_CREDENTIAL_CONFIG_INVALID',
'SOURCE_CREDENTIAL_MUTATION_FAILED',
@ -1075,6 +1080,7 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({
'SOURCE_SECRET_INTEGRITY_FAILED',
'SOURCE_SECRET_REF_CONFLICT',
'SOURCE_SYNC_FAILED',
'SOURCE_SYNC_SELECTION_MISMATCH',
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
'SOURCE_WEBSITE_CRAWL_FAILED',
'SOURCE_WORKFLOW_FAILED',
@ -1170,6 +1176,8 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({
'RESEARCH_TASK_FAILED',
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
'RETRIEVAL_DELETION_IN_PROGRESS',
'RETRIEVAL_EXECUTION_LEASE_LOST',
'SOURCE_BULK_ACTION_FAILED',
'SOURCE_CREDENTIAL_CONFIG_INVALID',
'SOURCE_CREDENTIAL_MUTATION_FAILED',
@ -1190,6 +1198,7 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({
'SOURCE_SECRET_INTEGRITY_FAILED',
'SOURCE_SECRET_REF_CONFLICT',
'SOURCE_SYNC_FAILED',
'SOURCE_SYNC_SELECTION_MISMATCH',
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
'SOURCE_WEBSITE_CRAWL_FAILED',
'SOURCE_WORKFLOW_FAILED',
@ -1248,6 +1257,7 @@ export const zKnowledgeFsBackgroundTaskResponse = z.object({
progress_percent: z.int().gte(0).lte(100),
progress_total: z.int().gte(0),
source_id: z.string().nullish(),
source_title: z.string().nullish(),
state: z.enum(['canceled', 'completed', 'failed', 'queued', 'running']),
task_kind: z.enum(['document', 'document_bulk', 'source']),
updated_at: z.iso.datetime(),
@ -1936,6 +1946,8 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({
'RESEARCH_TASK_FAILED',
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
'RETRIEVAL_DELETION_IN_PROGRESS',
'RETRIEVAL_EXECUTION_LEASE_LOST',
'SOURCE_BULK_ACTION_FAILED',
'SOURCE_CREDENTIAL_CONFIG_INVALID',
'SOURCE_CREDENTIAL_MUTATION_FAILED',
@ -1956,6 +1968,7 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({
'SOURCE_SECRET_INTEGRITY_FAILED',
'SOURCE_SECRET_REF_CONFLICT',
'SOURCE_SYNC_FAILED',
'SOURCE_SYNC_SELECTION_MISMATCH',
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
'SOURCE_WEBSITE_CRAWL_FAILED',
'SOURCE_WORKFLOW_FAILED',

View File

@ -1687,6 +1687,8 @@ export type KnowledgeFsPublicFailureResponse = {
| 'RESEARCH_TASK_FAILED'
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
| 'RETRIEVAL_DELETION_IN_PROGRESS'
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
| 'SOURCE_BULK_ACTION_FAILED'
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
@ -1707,6 +1709,7 @@ export type KnowledgeFsPublicFailureResponse = {
| 'SOURCE_SECRET_INTEGRITY_FAILED'
| 'SOURCE_SECRET_REF_CONFLICT'
| 'SOURCE_SYNC_FAILED'
| 'SOURCE_SYNC_SELECTION_MISMATCH'
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
| 'SOURCE_WEBSITE_CRAWL_FAILED'
| 'SOURCE_WORKFLOW_FAILED'
@ -1977,6 +1980,8 @@ export type KnowledgeFsSourceCredentialTestResponse = {
| 'RESEARCH_TASK_FAILED'
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
| 'RETRIEVAL_DELETION_IN_PROGRESS'
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
| 'SOURCE_BULK_ACTION_FAILED'
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
@ -1997,6 +2002,7 @@ export type KnowledgeFsSourceCredentialTestResponse = {
| 'SOURCE_SECRET_INTEGRITY_FAILED'
| 'SOURCE_SECRET_REF_CONFLICT'
| 'SOURCE_SYNC_FAILED'
| 'SOURCE_SYNC_SELECTION_MISMATCH'
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
| 'SOURCE_WEBSITE_CRAWL_FAILED'
| 'SOURCE_WORKFLOW_FAILED'
@ -2106,6 +2112,8 @@ export type KnowledgeFsSourceImportFailureResponse = {
| 'RESEARCH_TASK_FAILED'
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
| 'RETRIEVAL_DELETION_IN_PROGRESS'
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
| 'SOURCE_BULK_ACTION_FAILED'
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
@ -2126,6 +2134,7 @@ export type KnowledgeFsSourceImportFailureResponse = {
| 'SOURCE_SECRET_INTEGRITY_FAILED'
| 'SOURCE_SECRET_REF_CONFLICT'
| 'SOURCE_SYNC_FAILED'
| 'SOURCE_SYNC_SELECTION_MISMATCH'
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
| 'SOURCE_WEBSITE_CRAWL_FAILED'
| 'SOURCE_WORKFLOW_FAILED'
@ -2308,6 +2317,8 @@ export type KnowledgeFsSourceWorkflowResponse = {
| 'RESEARCH_TASK_FAILED'
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
| 'RETRIEVAL_DELETION_IN_PROGRESS'
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
| 'SOURCE_BULK_ACTION_FAILED'
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
@ -2328,6 +2339,7 @@ export type KnowledgeFsSourceWorkflowResponse = {
| 'SOURCE_SECRET_INTEGRITY_FAILED'
| 'SOURCE_SECRET_REF_CONFLICT'
| 'SOURCE_SYNC_FAILED'
| 'SOURCE_SYNC_SELECTION_MISMATCH'
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
| 'SOURCE_WEBSITE_CRAWL_FAILED'
| 'SOURCE_WORKFLOW_FAILED'

View File

@ -1944,6 +1944,8 @@ export const zKnowledgeFsPublicFailureResponse = z.object({
'RESEARCH_TASK_FAILED',
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
'RETRIEVAL_DELETION_IN_PROGRESS',
'RETRIEVAL_EXECUTION_LEASE_LOST',
'SOURCE_BULK_ACTION_FAILED',
'SOURCE_CREDENTIAL_CONFIG_INVALID',
'SOURCE_CREDENTIAL_MUTATION_FAILED',
@ -1964,6 +1966,7 @@ export const zKnowledgeFsPublicFailureResponse = z.object({
'SOURCE_SECRET_INTEGRITY_FAILED',
'SOURCE_SECRET_REF_CONFLICT',
'SOURCE_SYNC_FAILED',
'SOURCE_SYNC_SELECTION_MISMATCH',
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
'SOURCE_WEBSITE_CRAWL_FAILED',
'SOURCE_WORKFLOW_FAILED',
@ -2318,6 +2321,8 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({
'RESEARCH_TASK_FAILED',
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
'RETRIEVAL_DELETION_IN_PROGRESS',
'RETRIEVAL_EXECUTION_LEASE_LOST',
'SOURCE_BULK_ACTION_FAILED',
'SOURCE_CREDENTIAL_CONFIG_INVALID',
'SOURCE_CREDENTIAL_MUTATION_FAILED',
@ -2338,6 +2343,7 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({
'SOURCE_SECRET_INTEGRITY_FAILED',
'SOURCE_SECRET_REF_CONFLICT',
'SOURCE_SYNC_FAILED',
'SOURCE_SYNC_SELECTION_MISMATCH',
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
'SOURCE_WEBSITE_CRAWL_FAILED',
'SOURCE_WORKFLOW_FAILED',
@ -2481,6 +2487,8 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({
'RESEARCH_TASK_FAILED',
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
'RETRIEVAL_DELETION_IN_PROGRESS',
'RETRIEVAL_EXECUTION_LEASE_LOST',
'SOURCE_BULK_ACTION_FAILED',
'SOURCE_CREDENTIAL_CONFIG_INVALID',
'SOURCE_CREDENTIAL_MUTATION_FAILED',
@ -2501,6 +2509,7 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({
'SOURCE_SECRET_INTEGRITY_FAILED',
'SOURCE_SECRET_REF_CONFLICT',
'SOURCE_SYNC_FAILED',
'SOURCE_SYNC_SELECTION_MISMATCH',
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
'SOURCE_WEBSITE_CRAWL_FAILED',
'SOURCE_WORKFLOW_FAILED',
@ -2683,6 +2692,8 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({
'RESEARCH_TASK_FAILED',
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
'RETRIEVAL_DELETION_IN_PROGRESS',
'RETRIEVAL_EXECUTION_LEASE_LOST',
'SOURCE_BULK_ACTION_FAILED',
'SOURCE_CREDENTIAL_CONFIG_INVALID',
'SOURCE_CREDENTIAL_MUTATION_FAILED',
@ -2703,6 +2714,7 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({
'SOURCE_SECRET_INTEGRITY_FAILED',
'SOURCE_SECRET_REF_CONFLICT',
'SOURCE_SYNC_FAILED',
'SOURCE_SYNC_SELECTION_MISMATCH',
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
'SOURCE_WEBSITE_CRAWL_FAILED',
'SOURCE_WORKFLOW_FAILED',

View File

@ -250,6 +250,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({
progress_percent: item.progressPercent,
progress_total: item.progressTotal ?? 1,
source_id: item.sourceId ?? null,
source_title: item.sourceTitle ?? null,
state:
item.state === 'succeeded'
? 'completed'
@ -4125,17 +4126,24 @@ describe('DocumentsPage', () => {
it('shows document, bulk re-index, and source tasks returned by the task list', async () => {
const user = userEvent.setup()
documentsQuery.data = { pages: [{ items: [document({})] }] }
sourcesQuery.data = { pages: [{ items: [] }] }
tasksQuery.data = {
pages: [
{
items: [
task({ id: 'document-task', state: 'succeeded' }),
backgroundTask({ id: 'reindex-task', progressCompleted: 12, progressTotal: 12 }),
backgroundTask({
documentId: 'document-1',
id: 'reindex-task',
progressCompleted: 1,
progressTotal: 1,
}),
backgroundTask({
errorMessage: 'Source sync failed',
id: 'source-task',
operation: 'source_sync',
sourceId: 'source-1',
sourceTitle: 'Notion support SOP',
state: 'failed',
taskKind: 'source',
}),
@ -4154,13 +4162,14 @@ describe('DocumentsPage', () => {
const panel = screen.getByRole('dialog', { name: 'dataset.newKnowledge.backgroundTasks' })
expect(within(panel).getAllByRole('listitem')).toHaveLength(3)
expect(
within(panel).getByText('dataset.newKnowledge.reindexDocuments · 12'),
within(panel).getByText('dataset.newKnowledge.reindexDocuments · sso-enterprise.pdf'),
).toBeInTheDocument()
expect(
within(panel).getByText(
'dataset.newKnowledge.overview.operation.source_sync · Notion support SOP',
),
).toBeInTheDocument()
expect(within(panel).queryByText(/ · 1$/)).not.toBeInTheDocument()
expect(within(panel).getByText('dataset.newKnowledge.taskFailure.internal')).toBeInTheDocument()
})

View File

@ -238,6 +238,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({
completed_at: item.completedAt ?? null,
created_at: item.createdAt,
document_id: item.documentId ?? null,
document_title: item.documentTitle ?? null,
document_revision: item.documentRevision ?? null,
error_code: item.errorCode ?? null,
error_message: item.errorMessage ?? null,
@ -250,6 +251,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({
progress_percent: item.progressPercent,
progress_total: item.progressTotal ?? 1,
source_id: item.sourceId ?? null,
source_title: item.sourceTitle ?? null,
state:
item.state === 'succeeded'
? 'completed'
@ -2034,7 +2036,7 @@ describe('DocumentDetailPage', () => {
id: 'another-task',
state: 'succeeded',
}),
backgroundTask({ id: 'bulk-reindex-task' }),
backgroundTask({ documentId: 'document-1', id: 'bulk-reindex-task' }),
],
},
],
@ -2059,7 +2061,7 @@ describe('DocumentDetailPage', () => {
})
expect(within(taskDrawer).getAllByText(/dataset\.newKnowledge\.addDocument/)).toHaveLength(2)
expect(
within(taskDrawer).getByText('dataset.newKnowledge.reindexDocuments · 1'),
within(taskDrawer).getByText('dataset.newKnowledge.reindexDocuments · sso-enterprise.pdf'),
).toBeInTheDocument()
})

View File

@ -117,7 +117,7 @@ export function DocumentTaskRow({
: task.operation === 'document_upload'
? `${t(($) => $['newKnowledge.addDocument'])}${progress ? ` · ${progress.total}` : ''}`
: task.operation === 'document_reindex'
? `${t(($) => $['newKnowledge.reindexDocuments'])}${progress ? ` · ${progress.total}` : resolvedDocumentTitle ? ` · ${resolvedDocumentTitle}` : ''}`
? `${t(($) => $['newKnowledge.reindexDocuments'])}${resolvedDocumentTitle ? ` · ${resolvedDocumentTitle}` : progress && progress.total > 1 ? ` · ${progress.total}` : ''}`
: task.operation === 'document_delete' && resolvedDocumentTitle
? `${operationTitle} · ${resolvedDocumentTitle}`
: progress
@ -199,8 +199,12 @@ export function DocumentTaskRow({
}
/>
<div className="min-w-0 flex-1">
<p className="truncate system-sm-medium text-text-primary">{title}</p>
<p className="mt-0.75 truncate system-xs-regular text-text-tertiary">{status}</p>
<p className="truncate system-sm-medium text-text-primary" title={title}>
{title}
</p>
<p className="mt-0.75 truncate system-xs-regular text-text-tertiary" title={status}>
{status}
</p>
{taskError && (
<p className="mt-1 system-2xs-regular wrap-break-word whitespace-pre-wrap text-text-destructive">
{taskError}

View File

@ -111,6 +111,7 @@ export type BackgroundTask = {
| 'canceled'
| 'superseded'
sourceId?: string
sourceTitle?: string
taskKind: KnowledgeFsBackgroundTaskResponse['task_kind']
updatedAt: string
}
@ -268,6 +269,7 @@ export function backgroundTaskFromApi(task: KnowledgeFsBackgroundTaskResponse):
? 'canceled'
: task.state,
sourceId: task.source_id ?? undefined,
sourceTitle: task.source_title ?? undefined,
taskKind: task.task_kind,
updatedAt: task.updated_at,
}

View File

@ -204,14 +204,16 @@ function useDocumentsTaskRowTitle(task: BackgroundTask) {
: task.operation === 'document_upload'
? `${t(($) => $['newKnowledge.addDocument'])}${progress ? ` · ${progress.total}` : ''}`
: task.operation === 'document_reindex'
? `${t(($) => $['newKnowledge.reindexDocuments'])}${progress ? ` · ${progress.total}` : documentTitle ? ` · ${documentTitle}` : ''}`
? `${t(($) => $['newKnowledge.reindexDocuments'])}${documentTitle ? ` · ${documentTitle}` : progress && progress.total > 1 ? ` · ${progress.total}` : ''}`
: task.operation === 'document_delete' && documentTitle
? `${operationTitle} · ${documentTitle}`
: sourceTitle
? `${operationTitle} · ${sourceTitle}`
: progress
? `${operationTitle} · ${progress.total}`
: operationTitle
: task.operation === 'source_sync'
? operationTitle
: progress
? `${operationTitle} · ${progress.total}`
: operationTitle
}
function DocumentsTaskDetails({ task }: { task: BackgroundTask }) {
@ -266,8 +268,12 @@ function DocumentsTaskDetails({ task }: { task: BackgroundTask }) {
}
/>
<div className="min-w-0 flex-1">
<p className="truncate system-sm-medium text-text-primary">{title}</p>
<p className="mt-0.75 truncate system-xs-regular text-text-tertiary">{status}</p>
<p className="truncate system-sm-medium text-text-primary" title={title}>
{title}
</p>
<p className="mt-0.75 truncate system-xs-regular text-text-tertiary" title={status}>
{status}
</p>
{taskError && (
<p className="mt-1 system-2xs-regular wrap-break-word whitespace-pre-wrap text-text-destructive">
{taskError}

View File

@ -119,7 +119,8 @@ export const createTaskDrawerRowLabelsAtom = (task: BackgroundTask) => {
documentTitlePending: Boolean(
task.documentId && !task.documentTitle && get(taskDrawerDocumentsPendingAtom),
),
sourceTitle: task.sourceId ? get(sourceNamesAtom).get(task.sourceId) : undefined,
sourceTitle:
task.sourceTitle ?? (task.sourceId ? get(sourceNamesAtom).get(task.sourceId) : undefined),
}))
return selectAtom(
labelsAtom,

View File

@ -58,6 +58,8 @@ const failureMessageKeyByCode = {
RESEARCH_TASK_FAILED: 'newKnowledge.taskFailure.research',
RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID: 'newKnowledge.taskFailure.access',
RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID: 'newKnowledge.taskFailure.research',
RETRIEVAL_DELETION_IN_PROGRESS: 'newKnowledge.taskFailure.conflict',
RETRIEVAL_EXECUTION_LEASE_LOST: 'newKnowledge.taskFailure.conflict',
SOURCE_BULK_ACTION_FAILED: 'newKnowledge.taskFailure.source',
SOURCE_CREDENTIAL_CONFIG_INVALID: 'newKnowledge.taskFailure.source',
SOURCE_CREDENTIAL_MUTATION_FAILED: 'newKnowledge.taskFailure.source',
@ -78,6 +80,7 @@ const failureMessageKeyByCode = {
SOURCE_SECRET_INTEGRITY_FAILED: 'newKnowledge.taskFailure.source',
SOURCE_SECRET_REF_CONFLICT: 'newKnowledge.taskFailure.conflict',
SOURCE_SYNC_FAILED: 'newKnowledge.taskFailure.source',
SOURCE_SYNC_SELECTION_MISMATCH: 'newKnowledge.taskFailure.conflict',
SOURCE_WEBSITE_CRAWL_CONFIG_INVALID: 'newKnowledge.taskFailure.source',
SOURCE_WEBSITE_CRAWL_FAILED: 'newKnowledge.taskFailure.source',
SOURCE_WORKFLOW_FAILED: 'newKnowledge.taskFailure.source',