diff --git a/apps/backend/cmd/plugin-fixture/fixture-package/ui/bundle.js b/apps/backend/cmd/plugin-fixture/fixture-package/ui/bundle.js index ea4551f3aa..3bc29182fe 100644 --- a/apps/backend/cmd/plugin-fixture/fixture-package/ui/bundle.js +++ b/apps/backend/cmd/plugin-fixture/fixture-package/ui/bundle.js @@ -21,7 +21,10 @@ * round-trips a single per-user document through host.storage * (get on mount, debounced set, subscribe to pick up a write from another * tab/surface), task-card indicator/tag components, generic task-row metadata, - * and task-menu actions under the "edit" and "primary" groups. It also + * and task-menu actions under the "edit" and "primary" groups. It registers a + * task-list facet (registerTaskListFacet) whose values a spec drives through + * `window.__e2eFacetValues`, so /tasks facet sort and grouping can be exercised + * against real plugin registrations. It also * registers one composer action on all three composer slots * (chat-input-actions, task-create-input-actions, new-session-input-actions), * which is how the e2e suite exercises PluginComposerCapability against real @@ -34,6 +37,12 @@ (function () { var moduleCount = 0; var listeners = new Set(); + // Task-list facet plumbing. Values come from `window.__e2eFacetValues` + // (taskId -> [{value,label,color}]) so a spec can drive multi-value + // membership, colors, and the untagged fallback against task ids the + // fixture cannot know ahead of time. `window.__e2eFacetNotify()` fires + // the subscription so the reactive re-read path is exercised for real. + var facetListeners = new Set(); function emit() { listeners.forEach(function (fn) { @@ -689,6 +698,26 @@ Component: NotesPanel, mobileEnabled: true, }); + registry.registerTaskListFacet({ + id: "fixture-tags", + label: "Fixture tag", + getValues: function (context) { + var byTask = window.__e2eFacetValues || {}; + if (byTask.__throwFor === context.taskId) throw new Error("fixture facet boom"); + return byTask[context.taskId] || []; + }, + subscribe: function (listener) { + facetListeners.add(listener); + window.__e2eFacetNotify = function () { + facetListeners.forEach(function (fn) { + fn(); + }); + }; + return function () { + facetListeners.delete(listener); + }; + }, + }); registry.registerComponent("chat-input-actions", ComposerAction); registry.registerComponent("task-create-input-actions", ComposerAction); registry.registerComponent("new-session-input-actions", ComposerAction); diff --git a/apps/packages/plugin-sdk/src/index.ts b/apps/packages/plugin-sdk/src/index.ts index ce15726504..24ee082758 100644 --- a/apps/packages/plugin-sdk/src/index.ts +++ b/apps/packages/plugin-sdk/src/index.ts @@ -304,6 +304,20 @@ export interface TaskFilterRegistration { matches(context: { taskId: string }, selected: string[]): boolean; } +export interface TaskListFacetValue { + value: string; + label: string; + color?: string; +} + +/** A synchronous, page-local facet contribution for the host task list. */ +export interface TaskListFacetRegistration { + id: string; + label: string; + getValues(context: { taskId: string; workspaceId?: string }): readonly TaskListFacetValue[]; + subscribe?(listener: () => void): () => void; +} + export type PluginStorageScope = "instance" | "workspace" | "task" | "session" | "repository"; export interface PluginStorageEntry { @@ -692,6 +706,7 @@ export interface PluginRegistry { registerTaskPanel(registration: TaskPanelRegistration): void; registerTaskMenuAction(registration: TaskMenuActionRegistration): void; registerTaskFilter(registration: TaskFilterRegistration): void; + registerTaskListFacet(registration: TaskListFacetRegistration): void; } export type PluginHost = PluginHostApi; diff --git a/apps/web/app/tasks/mobile-tasks-actions.tsx b/apps/web/app/tasks/mobile-tasks-actions.tsx new file mode 100644 index 0000000000..ca86b1ea6f --- /dev/null +++ b/apps/web/app/tasks/mobile-tasks-actions.tsx @@ -0,0 +1,32 @@ +import { MobileFab } from "@/components/kanban/mobile-fab"; +import { MobileTasksCreateDialog, type MobileTaskStep } from "./mobile-tasks-create-dialog"; + +export function MobileTasksActions({ + workspaceId, + workflowId, + steps, + open, + onOpenChange, + onCreated, +}: { + workspaceId: string; + workflowId: string | null; + steps: MobileTaskStep[]; + open: boolean; + onOpenChange: (open: boolean) => void; + onCreated: () => Promise; +}) { + return ( + <> + onOpenChange(true)} /> + + + ); +} diff --git a/apps/web/app/tasks/mobile-tasks-create-dialog.tsx b/apps/web/app/tasks/mobile-tasks-create-dialog.tsx new file mode 100644 index 0000000000..122e277920 --- /dev/null +++ b/apps/web/app/tasks/mobile-tasks-create-dialog.tsx @@ -0,0 +1,42 @@ +import { TaskCreateDialog } from "@/components/task-create-dialog"; + +export type MobileTaskStep = { + id: string; + title: string; + events?: { + on_enter?: Array<{ type: string; config?: Record }>; + on_turn_complete?: Array<{ type: string; config?: Record }>; + }; +}; + +export function MobileTasksCreateDialog({ + open, + onOpenChange, + workspaceId, + workflowId, + steps, + onCreated, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + workspaceId: string; + workflowId: string | null; + steps: MobileTaskStep[]; + onCreated: () => Promise; +}) { + return ( + { + onOpenChange(false); + void onCreated(); + }} + /> + ); +} diff --git a/apps/web/app/tasks/tasks-list-controls.tsx b/apps/web/app/tasks/tasks-list-controls.tsx index 46b0c827c1..c8124e47d5 100644 --- a/apps/web/app/tasks/tasks-list-controls.tsx +++ b/apps/web/app/tasks/tasks-list-controls.tsx @@ -9,8 +9,6 @@ import { SORT_OPTION_LABEL_KEYS, TASKS_LIST_GROUP_OPTIONS, TASKS_LIST_SORT_OPTIONS, - type TasksListGroup, - type TasksListSort, } from "@/lib/tasks/tasks-list-options"; export function TasksListControls({ @@ -20,37 +18,45 @@ export function TasksListControls({ onTasksListSortChange, tasksListGroup, onTasksListGroupChange, + facetOptions, }: { showArchived: boolean; onShowArchivedChange: (show: boolean) => void; - tasksListSort: TasksListSort; - onTasksListSortChange: (sort: TasksListSort) => void; - tasksListGroup: TasksListGroup; - onTasksListGroupChange: (group: TasksListGroup) => void; + tasksListSort: string; + onTasksListSortChange: (sort: string) => void; + tasksListGroup: string; + onTasksListGroupChange: (group: string) => void; + facetOptions?: ReadonlyArray<{ value: string; label: string }>; }) { const { t } = useTranslation(); - const sortOptions = TASKS_LIST_SORT_OPTIONS.map((option) => ({ - value: option.value, - label: t(SORT_OPTION_LABEL_KEYS[option.value]), - })); - const groupOptions = TASKS_LIST_GROUP_OPTIONS.map((option) => ({ - value: option.value, - label: t(GROUP_OPTION_LABEL_KEYS[option.value]), - })); + const sortOptions = [ + ...TASKS_LIST_SORT_OPTIONS.map((option) => ({ + value: option.value, + label: t(SORT_OPTION_LABEL_KEYS[option.value]), + })), + ...(facetOptions ?? []), + ]; + const groupOptions = [ + ...TASKS_LIST_GROUP_OPTIONS.map((option) => ({ + value: option.value, + label: t(GROUP_OPTION_LABEL_KEYS[option.value]), + })), + ...(facetOptions ?? []), + ]; return (
onTasksListSortChange(value as TasksListSort)} + onChange={onTasksListSortChange} testId="tasks-list-sort" /> onTasksListGroupChange(value as TasksListGroup)} + onChange={onTasksListGroupChange} testId="tasks-list-group" />