-
+
- {row.label}
+ {job.label}
{durationCell}
- {row.kind}
- {row.detail ?? status}
+ {job.kind}
+ {job.detail ?? status}
{observable ? : null}
@@ -254,10 +183,10 @@ function TaskItem({ row, view, expanded, now, onToggle, t }: {
)
: (
<>
-
- {row.kind}
- {row.label}
- {row.detail ?? status}
+
+ {job.kind}
+ {job.label}
+ {job.detail ?? status}
{durationCell}
{observable ? : null}
>
@@ -270,7 +199,7 @@ function TaskItem({ row, view, expanded, now, onToggle, t }: {
type="button"
className={live ? css.row : `${css.row} ${css.rowSettled}`}
aria-expanded={expanded}
- aria-label={t(expanded ? 'row.collapseAria' : 'row.expandAria', { label: row.label })}
+ aria-label={t(expanded ? 'row.collapseAria' : 'row.expandAria', { label: job.label })}
onClick={onToggle}
>
{body}
@@ -289,10 +218,10 @@ function TaskItem({ row, view, expanded, now, onToggle, t }: {
? {t('output.error', { error: view.error })}
: null}
state.jobsBySession[sessionId]) ?? NO_JOBS
- const owned = useActivity(state => state.rowsBySession[sessionId]) ?? NO_ROWS
- const unowned = useActivity(state => state.rowsBySession['']) ?? NO_ROWS
- const observedViews = useActivity(state => state.observed)
+ const observedViews = useJobOutput(state => state.observed)
const [open, setOpen] = useState(false)
const [expandedKey, setExpandedKey] = useState(undefined)
const [now, setNow] = useState(() => Date.now())
@@ -333,12 +259,9 @@ export function ActivityListAction({ sessionId, useSessions, useActivity, observ
// inside the viewport (the stylesheet alone cannot see the anchor offset).
const [menuShift, setMenuShift] = useState(0)
- const rows = useMemo(
- () => ordered(mergeRows(jobs, [...owned, ...unowned])),
- [jobs, owned, unowned],
- )
+ const rows = useMemo(() => ordered(jobs), [jobs])
const liveRows = useMemo(() => rows.filter(isLive), [rows])
- const settledRows = useMemo(() => rows.filter(row => !isLive(row)), [rows])
+ const settledRows = useMemo(() => rows.filter(job => !isLive(job)), [rows])
useDismissOnOutsidePointer(rootRef, open, setOpen)
@@ -378,15 +301,16 @@ export function ActivityListAction({ sessionId, useSessions, useActivity, observ
// Observation follows visibility: the stream opens when an observable panel
// expands and closes when it collapses, unmounts, or the popover closes.
- const activeActivity = open && expandedKey !== undefined
- ? rows.find(row => row.key === expandedKey)?.activityId
+ const expandedRow = open && expandedKey !== undefined
+ ? rows.find(job => String(job.id) === expandedKey)
: undefined
+ const activeJob = expandedRow !== undefined && isObservable(expandedRow) ? expandedRow.id : undefined
useEffect(() => {
- if (activeActivity === undefined) return
- return observe(activeActivity)
- }, [activeActivity, observe])
+ if (activeJob === undefined) return
+ return observe(sessionId, activeJob)
+ }, [sessionId, activeJob, observe])
- // The last task disappearing removes this control; close first so focus
+ // The last job disappearing removes this control; close first so focus
// does not vanish from an unmounting node.
useEffect(() => {
if (rows.length === 0 && open) setOpen(false)
@@ -394,7 +318,7 @@ export function ActivityListAction({ sessionId, useSessions, useActivity, observ
// An expanded row that left the list (owner disposal) folds its panel.
useEffect(() => {
- if (expandedKey !== undefined && !rows.some(row => row.key === expandedKey)) {
+ if (expandedKey !== undefined && !rows.some(job => String(job.id) === expandedKey)) {
setExpandedKey(undefined)
}
}, [rows, expandedKey])
@@ -413,15 +337,15 @@ export function ActivityListAction({ sessionId, useSessions, useActivity, observ
triggerRef.current?.focus()
}
- const item = (row: TaskRow) => (
- (
+ {
- setExpandedKey(current => current === row.key ? undefined : row.key)
+ setExpandedKey(current => current === String(job.id) ? undefined : String(job.id))
}}
t={t}
/>
@@ -437,7 +361,7 @@ export function ActivityListAction({ sessionId, useSessions, useActivity, observ
aria-label={countLabel}
onClick={() => {
// Sample the clock in the same commit that opens the list: the
- // mount-time value predates every task, so the first painted frame
+ // mount-time value predates every job, so the first painted frame
// would otherwise clamp a long-running row to zero until the
// open effect corrects it a frame later.
setNow(Date.now())
diff --git a/packages/client/ui-jobs/src/client/index.ts b/packages/client/ui-jobs/src/client/index.ts
new file mode 100644
index 0000000000..164b6f6e3c
--- /dev/null
+++ b/packages/client/ui-jobs/src/client/index.ts
@@ -0,0 +1,49 @@
+/**
+ * Background-job plugin, browser half: contributes one session-header action
+ * that renders this session's jobs. Job rows arrive through the
+ * `jobsBySession` list mirror; per-row observation streams arrive through the
+ * `jobOutput` client service. This plugin holds no transport state of its
+ * own.
+ */
+import type { Context as ClientContext } from '@deepseek-ai/cordis'
+import { JobListAction } from './JobListAction.tsx'
+import type { JobListInjected } from './JobListAction.tsx'
+import type {} from '@deepseek-ai/dsh-api-session-controller/client'
+import type {} from '@deepseek-ai/dsh-client-locale/client'
+import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
+import type {} from '@deepseek-ai/dsh-client-ui-session/client'
+import { en, NS, zh, type JobKey } from './locales.ts'
+
+declare module '@deepseek-ai/dsh-client-ui-slots' {
+ interface LocaleNamespaceMap {
+ /** Background-job list copy. */
+ 'job': JobKey
+ }
+}
+
+export type { JobListActionProps, JobListInjected } from './JobListAction.tsx'
+
+/** Required services: session state, job observation, the slot registry, and dictionaries. */
+export const inject = ['sessions', 'jobOutput', 'slots', 'locale']
+
+/**
+ * Client plugin body: register the dictionaries and the header action.
+ * @param ctx - client root context.
+ */
+export function apply(ctx: ClientContext): void {
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-jobs: dictionaries')
+ ctx.slots.inject(
+ 'conversation.session.header.actions',
+ () => ctx.slots.register({
+ name: 'conversation.session.header.actions',
+ id: 'job-list',
+ // After the subagent catalog: session lineage reads before running work.
+ order: 20,
+ locale: NS,
+ inject: (): JobListInjected => ({
+ hooks: { jobOutput: ctx.jobOutput.state },
+ observe: (sessionId, id) => ctx.jobOutput.observe(sessionId, id),
+ }),
+ }, JobListAction),
+ )
+}
diff --git a/packages/client/ui-activity/src/client/locales.ts b/packages/client/ui-jobs/src/client/locales.ts
similarity index 78%
rename from packages/client/ui-activity/src/client/locales.ts
rename to packages/client/ui-jobs/src/client/locales.ts
index d4fb08ec31..d2d36b74d1 100644
--- a/packages/client/ui-activity/src/client/locales.ts
+++ b/packages/client/ui-jobs/src/client/locales.ts
@@ -1,15 +1,15 @@
-/** `activity` namespace dictionaries. */
+/** `job` namespace dictionaries. */
/** Dictionary namespace owned by this plugin. */
-export const NS = 'activity'
+export const NS = 'job'
/** Simplified Chinese dictionary (the key-set source of truth). */
export const zh = {
- 'count.live.one': '{count} 个任务进行中',
- 'count.live.other': '{count} 个任务进行中',
- 'count.idle.one': '{count} 个任务',
- 'count.idle.other': '{count} 个任务',
- 'list.aria': '任务',
+ 'count.live.one': '{count} 个后台任务运行中',
+ 'count.live.other': '{count} 个后台任务运行中',
+ 'count.idle.one': '{count} 个后台任务',
+ 'count.idle.other': '{count} 个后台任务',
+ 'list.aria': '后台任务',
'section.live': '进行中',
'section.settled': '已结束',
'row.expandAria': '展开 {label} 的实时输出',
@@ -41,12 +41,12 @@ export const zh = {
} as const
/** English dictionary, key-identical to the Chinese source of truth. */
-export const en: Record = {
- 'count.live.one': '{count} task running',
- 'count.live.other': '{count} tasks running',
- 'count.idle.one': '{count} task',
- 'count.idle.other': '{count} tasks',
- 'list.aria': 'Tasks',
+export const en: Record = {
+ 'count.live.one': '{count} background job running',
+ 'count.live.other': '{count} background jobs running',
+ 'count.idle.one': '{count} background job',
+ 'count.idle.other': '{count} background jobs',
+ 'list.aria': 'Background jobs',
'section.live': 'Running',
'section.settled': 'Finished',
'row.expandAria': 'Show live output of {label}',
@@ -77,5 +77,5 @@ export const en: Record = {
'terminal.expandAria': 'Expand {n} collapsed output lines',
}
-/** Key domain of the `activity` namespace (zh is the source of truth). */
-export type ActivityKey = keyof typeof zh
+/** Key domain of the `job` namespace (zh is the source of truth). */
+export type JobKey = keyof typeof zh
diff --git a/packages/client/ui-activity/src/css-modules.d.ts b/packages/client/ui-jobs/src/css-modules.d.ts
similarity index 100%
rename from packages/client/ui-activity/src/css-modules.d.ts
rename to packages/client/ui-jobs/src/css-modules.d.ts
diff --git a/packages/client/ui-activity/src/index.ts b/packages/client/ui-jobs/src/index.ts
similarity index 66%
rename from packages/client/ui-activity/src/index.ts
rename to packages/client/ui-jobs/src/index.ts
index f75578ddaf..457e7eeb9c 100644
--- a/packages/client/ui-activity/src/index.ts
+++ b/packages/client/ui-jobs/src/index.ts
@@ -1,8 +1,8 @@
/**
- * Node half of the live-activity list plugin. The browser half in
+ * Node half of the background-job list plugin. The browser half in
* `src/client/` owns every contribution; this entry exists so the package
* appears as an ordinary Loader row.
- * @module @deepseek-ai/dsh-client-ui-activity
+ * @module @deepseek-ai/dsh-client-ui-jobs
*/
/** Loader-visible no-op body; the browser half carries the feature. */
diff --git a/packages/client/ui-activity/src/invariant.ts b/packages/client/ui-jobs/src/invariant.ts
similarity index 70%
rename from packages/client/ui-activity/src/invariant.ts
rename to packages/client/ui-jobs/src/invariant.ts
index 22eba24c42..51ae72f998 100644
--- a/packages/client/ui-activity/src/invariant.ts
+++ b/packages/client/ui-jobs/src/invariant.ts
@@ -1,20 +1,20 @@
/**
- * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-activity`.
- * @module @deepseek-ai/dsh-client-ui-activity/invariant
+ * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-jobs`.
+ * @module @deepseek-ai/dsh-client-ui-jobs/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
-const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-activity'
+const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-jobs'
/** Cordis companion plugin name. */
-export const name = 'client-ui-activity-invariant'
+export const name = 'client-ui-jobs-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
-/** No runtime invariant: pure presentation over the activity feed's client snapshot; the feed's own packages assert the data relations. */
+/** No runtime invariant: pure presentation over client snapshots; the registry packages assert the data relations. */
const install: InvariantInstaller = () => {}
/**
diff --git a/packages/client/ui-activity/tests/browser-plugin.client.spec.ts b/packages/client/ui-jobs/tests/browser-plugin.client.spec.ts
similarity index 73%
rename from packages/client/ui-activity/tests/browser-plugin.client.spec.ts
rename to packages/client/ui-jobs/tests/browser-plugin.client.spec.ts
index 3f84c2252d..8de72f8ccd 100644
--- a/packages/client/ui-activity/tests/browser-plugin.client.spec.ts
+++ b/packages/client/ui-jobs/tests/browser-plugin.client.spec.ts
@@ -1,5 +1,5 @@
/**
- * ui-activity plugin halves: the browser entry's dictionary and header-slot
+ * ui-jobs plugin halves: the browser entry's dictionary and header-slot
* registrations against the real SlotRegistry (with fiber teardown proving
* removal — HMR safety), the inert node entry, and the invariant companion's
* ownership reservation.
@@ -12,7 +12,7 @@ import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
import { apply as applyLocale, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client'
import { apply, inject } from '../src/client/index.ts'
import { apply as applyNode } from '../src/index.ts'
-import * as ActivityInvariant from '../src/invariant.ts'
+import * as JobsInvariant from '../src/invariant.ts'
import { en, NS, zh } from '../src/client/locales.ts'
/** Slot ledger reader: entry ids currently registered in the header list. */
@@ -22,8 +22,8 @@ function headerEntryIds(ctx: Context): (string | undefined)[] {
.map(entry => entry.options.id)
}
-/** Ids handed to the stubbed feed's observe control. */
-const observed: string[] = []
+/** Observation requests handed to the stubbed job-output control. */
+const observed: [string | undefined, string][] = []
/** Boot the browser half over a real slot tree that declares the header list. */
async function bench(): Promise<{ ctx: Context; fiber: ReturnType }> {
@@ -36,9 +36,12 @@ async function bench(): Promise<{ ctx: Context; fiber: ReturnType null)
ctx.provide('sessions', {} as never)
- ctx.provide('activityFeed', {
- state: { getSnapshot: () => ({ rowsBySession: {}, observed: {}, phase: 'pending' }), subscribe: () => () => {} },
- observe: (id: string) => { observed.push(id); return () => {} },
+ ctx.provide('jobOutput', {
+ state: { getSnapshot: () => ({ observed: {} }), subscribe: () => () => {} },
+ observe: (sessionId: string | undefined, id: string) => {
+ observed.push([sessionId, id])
+ return () => {}
+ },
} as never)
// The locale plugin binds a settings scope, which reads the connection handle
// and the forwarded-event port.
@@ -55,30 +58,33 @@ async function bench(): Promise<{ ctx: Context; fiber: ReturnType {
+describe('ui-jobs browser half', () => {
it('declares the services it binds', () => {
- expect(inject).toEqual(['sessions', 'activityFeed', 'slots', 'locale'])
+ expect(inject).toEqual(['sessions', 'jobOutput', 'slots', 'locale'])
})
- it('exposes the feed source and observation control through the inject face', async () => {
+ it('exposes the output source and observation control through the inject face', async () => {
const { ctx } = await bench()
const entry = ctx.slots
.entries('conversation.session.header.actions')
- .find(candidate => candidate.options.id === 'activity-list')
+ .find(candidate => candidate.options.id === 'job-list')
const face = (entry as unknown as {
- inject: () => { hooks: { activity: unknown }; observe: (id: string) => () => void }
+ inject: () => {
+ hooks: { jobOutput: unknown }
+ observe: (sessionId: string | undefined, id: string) => () => void
+ }
}).inject()
- expect(face.hooks.activity).toBeDefined()
- const stopper = face.observe('bash-1')
- expect(observed).toEqual(['bash-1'])
+ expect(face.hooks.jobOutput).toBeDefined()
+ const stopper = face.observe('session', 'bash-1')
+ expect(observed).toEqual([['session', 'bash-1']])
stopper()
})
it('registers the header action, and fiber teardown removes it (HMR safety)', async () => {
const { ctx, fiber } = await bench()
- expect(headerEntryIds(ctx)).toContain('activity-list')
+ expect(headerEntryIds(ctx)).toContain('job-list')
await fiber.dispose()
- expect(headerEntryIds(ctx)).not.toContain('activity-list')
+ expect(headerEntryIds(ctx)).not.toContain('job-list')
})
it('registers both dictionaries under its own namespace and releases them with the fiber', async () => {
@@ -98,19 +104,19 @@ describe('ui-activity browser half', () => {
})
})
-describe('ui-activity node half', () => {
+describe('ui-jobs node half', () => {
it('contributes no host behavior', () => {
// The node half exists only so the plugin appears in the Loader tree.
expect(applyNode).not.toThrow()
})
})
-describe('ui-activity invariant companion', () => {
+describe('ui-jobs invariant companion', () => {
it('reserves package ownership under its declared companion name', async () => {
const ctx = new Context()
await ctx.plugin(InvariantRegistry, { enabled: true })
- await ctx.plugin(ActivityInvariant)
- expect(ActivityInvariant.name).toBe('client-ui-activity-invariant')
- expect(ActivityInvariant.inject).toEqual(['invariants'])
+ await ctx.plugin(JobsInvariant)
+ expect(JobsInvariant.name).toBe('client-ui-jobs-invariant')
+ expect(JobsInvariant.inject).toEqual(['invariants'])
})
})
diff --git a/packages/client/ui-activity/tests/activity-list-action.client.spec.tsx b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx
similarity index 59%
rename from packages/client/ui-activity/tests/activity-list-action.client.spec.tsx
rename to packages/client/ui-jobs/tests/job-list-action.client.spec.tsx
index ffb4ce5492..15bd8ec4a9 100644
--- a/packages/client/ui-activity/tests/activity-list-action.client.spec.tsx
+++ b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx
@@ -2,12 +2,10 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
-import type {
- ActivityFeedSnapshot, ActivityId, ActivityRow, ObservedActivity,
-} from '@deepseek-ai/dsh-api-activity-controller/client'
+import type { JobOutputSnapshot, ObservedJob } from '@deepseek-ai/dsh-api-session-controller/client'
import type { SessionJob } from '@deepseek-ai/dsh-api-session-controller/types'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
-import { ActivityListAction, type ActivityListActionProps } from '../src/client/ActivityListAction.tsx'
+import { JobListAction, type JobListActionProps } from '../src/client/JobListAction.tsx'
import { zh } from '../src/client/locales.ts'
afterEach(() => {
@@ -17,26 +15,7 @@ afterEach(() => {
})
const SESSION = 'session' as SessionId
-const t: ActivityListActionProps['t'] = makeTranslate(zh)
-
-function row(over: Partial = {}): ActivityRow {
- return {
- id: 'bash-1' as ActivityId,
- kind: 'bash',
- label: 'pnpm run build',
- sessionId: SESSION,
- status: 'running',
- startedAt: 1_700_000_000_000,
- outputTotal: 0,
- ...over,
- }
-}
-
-/** A row with no owner session, as the unowned bucket carries them. */
-function unownedRow(over: Partial = {}): ActivityRow {
- const { sessionId: _drop, ...rest } = row(over)
- return rest
-}
+const t: JobListActionProps['t'] = makeTranslate(zh)
function job(over: Partial = {}): SessionJob {
return {
@@ -49,117 +28,88 @@ function job(over: Partial = {}): SessionJob {
}
}
+/** A job that declared an observation record, so its row offers a panel. */
+function recordJob(over: Partial = {}): SessionJob {
+ return job({ outputTotal: 0, ...over })
+}
+
function props(
- snapshot: Partial,
- observe: ActivityListActionProps['observe'] = () => () => {},
- jobs: readonly SessionJob[] = [],
-): ActivityListActionProps {
- const state: ActivityFeedSnapshot = {
- rowsBySession: {},
- observed: {},
- phase: 'ready',
- ...snapshot,
- }
- function useActivity(select: (value: ActivityFeedSnapshot) => T): T {
- return select(state)
+ jobs: readonly SessionJob[],
+ observe: JobListActionProps['observe'] = () => () => {},
+ observed: Readonly> = {},
+): JobListActionProps {
+ const outputState: JobOutputSnapshot = { observed }
+ function useJobOutput(select: (value: JobOutputSnapshot) => T): T {
+ return select(outputState)
}
const sessionsState = { jobsBySession: jobs.length > 0 ? { [SESSION]: jobs } : {} }
function useSessions(select: (value: typeof sessionsState) => T): T {
return select(sessionsState)
}
- return { sessionId: SESSION, useSessions, useActivity, observe, t } as unknown as ActivityListActionProps
+ return { sessionId: SESSION, useSessions, useJobOutput, observe, t } as unknown as JobListActionProps
}
function openList(): void {
fireEvent.click(screen.getAllByRole('button')[0]!)
}
-describe('ActivityListAction visibility', () => {
- it('renders nothing while the session sees no tasks', () => {
- const { container } = render()
+describe('JobListAction visibility', () => {
+ it('renders nothing while the session sees no jobs', () => {
+ const { container } = render()
expect(container.innerHTML).toBe('')
})
- it('counts live tasks on the trigger, merging owned and unowned activity rows', () => {
- render()
- expect(screen.getByRole('button', { name: '2 个任务进行中' })).toBeDefined()
+ it('counts live jobs on the trigger', () => {
+ render()
+ expect(screen.getByRole('button', { name: '2 个后台任务运行中' })).toBeDefined()
})
- it('falls back to the total when nothing is live and hides foreign sessions', () => {
- render()
- expect(screen.getByRole('button', { name: '1 个任务' })).toBeDefined()
+ it('falls back to the total when nothing is live', () => {
+ render()
+ expect(screen.getByRole('button', { name: '1 个后台任务' })).toBeDefined()
})
})
-describe('ActivityListAction merged rows', () => {
- it('joins a job with its correlated activity: job lifecycle, activity output', () => {
- const observe = vi.fn(() => () => {})
- render()
- openList()
- const list = screen.getByRole('list', { name: zh['list.aria'] })
- // One merged row: the job's label and lifecycle word, not the activity's.
- expect(within(list).getAllByRole('listitem')).toHaveLength(1)
- expect(within(list).getByText('pnpm run build')).toBeDefined()
- expect(screen.queryByText('activity-side label')).toBeNull()
- expect(within(list).getByText('winding down')).toBeDefined()
- // Expansion observes the correlated activity's stream.
- fireEvent.click(screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') }))
- expect(observe).toHaveBeenCalledWith('bash-act-7')
- })
-
- it('renders a bare job row without expansion affordances', () => {
- render( {
+ it('renders a record-less job row without expansion affordances', () => {
+ render()
- expect(screen.getByRole('button', { name: '1 个任务进行中' })).toBeDefined()
+ expect(screen.getByRole('button', { name: '1 个后台任务运行中' })).toBeDefined()
openList()
expect(screen.getByText('explore the repo')).toBeDefined()
expect(screen.queryByRole('button', { name: zh['row.expandAria'].replace('{label}', 'explore the repo') })).toBeNull()
})
- it('keeps an activity whose job is not in the projection', () => {
- render()
+ it('shows the live detail line in place of the status word', () => {
+ render()
openList()
- expect(screen.getByText('pnpm run build')).toBeDefined()
+ const list = screen.getByRole('list', { name: zh['list.aria'] })
+ expect(within(list).getAllByRole('listitem')).toHaveLength(1)
+ expect(within(list).getByText('pnpm run build')).toBeDefined()
+ expect(within(list).getByText('winding down')).toBeDefined()
})
it('labels each non-empty section and drops the heading of an empty one', () => {
const settled = [job({ id: 'bash-2' as SessionJob['id'], status: 'completed', finishedAt: 1_700_000_012_000 })]
- const { rerender } = render()
+ const { rerender } = render()
openList()
expect(screen.getByText(zh['section.live'])).toBeDefined()
expect(screen.getByText(zh['section.settled'])).toBeDefined()
// A live-only list keeps its own heading and drops the settled one.
- rerender()
+ rerender()
expect(screen.getByText(zh['section.live'])).toBeDefined()
expect(screen.queryByText(zh['section.settled'])).toBeNull()
})
it('shows a settled duration and ticks a live one', () => {
vi.useFakeTimers({ now: 1_700_000_020_000 })
- render( {
act(() => { vi.advanceTimersByTime(1_000) })
expect(screen.getByText('6秒')).toBeDefined()
})
-})
-describe('ActivityListAction rows and observation', () => {
it('lists rows with kind, label, and detail-or-status', () => {
- render()
+ render()
openList()
const list = screen.getByRole('list', { name: zh['list.aria'] })
const items = within(list).getAllByRole('listitem')
@@ -194,21 +138,52 @@ describe('ActivityListAction rows and observation', () => {
expect(items[1]?.textContent).toContain('exit code: 3')
})
+ it('orders live rows first by start and settled rows newest-first with tie-breaks', () => {
+ const settled = (id: string, label: string, startedAt: number, finishedAt?: number): SessionJob =>
+ job({
+ id: id as SessionJob['id'],
+ label,
+ status: 'completed',
+ startedAt,
+ ...finishedAt !== undefined ? { finishedAt } : {},
+ })
+ render()
+ openList()
+ const labels = within(screen.getByRole('list', { name: zh['list.aria'] }))
+ .getAllByRole('listitem')
+ .map(item => item.querySelector('[title]')?.getAttribute('title'))
+ expect(labels).toEqual([
+ 'live-2', 'live-1', 'settled-d', 'settled-a', 'settled-c', 'settled-b', 'settled-e',
+ ])
+ expect(screen.getAllByText(zh['status.completed']).length).toBeGreaterThan(0)
+ })
+})
+
+describe('JobListAction observation', () => {
it('marks killed rows, renders every output line unfolded, and copies the command', async () => {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
const lines = Array.from({ length: 24 }, (_value, index) => `line ${index + 1}`).join('\n')
- const view: ObservedActivity = {
- activityId: 'bash-1' as ActivityId,
+ const view: ObservedJob = {
+ jobId: 'bash-1' as SessionJob['id'],
text: lines,
gapBefore: false,
status: 'killed',
streaming: false,
}
- render()
+ render()
openList()
expect(screen.getByText(zh['status.killed'])).toBeDefined()
fireEvent.click(screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') }))
@@ -223,45 +198,13 @@ describe('ActivityListAction rows and observation', () => {
expect(writeText).toHaveBeenCalledWith('pnpm run build')
})
- it('orders live rows first by start and settled rows newest-first with tie-breaks', () => {
- const settled = (id: string, label: string, startedAt: number, finishedAt?: number): ActivityRow =>
- row({
- id: id as ActivityId,
- label,
- status: 'completed',
- startedAt,
- ...finishedAt !== undefined ? { finishedAt } : {},
- })
- render()
- openList()
- const labels = within(screen.getByRole('list', { name: zh['list.aria'] }))
- .getAllByRole('listitem')
- .map(item => item.querySelector('[title]')?.getAttribute('title'))
- expect(labels).toEqual([
- 'live-2', 'live-1', 'settled-d', 'settled-a', 'settled-c', 'settled-b', 'settled-e',
- ])
- expect(screen.getAllByText(zh['status.completed']).length).toBeGreaterThan(0)
- })
-
it('shifts an overflowing popover back inside the viewport and follows resizes', () => {
vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(440)
vi.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect').mockReturnValue({ left: 344 } as DOMRect)
const originalWidth = window.innerWidth
Object.defineProperty(window, 'innerWidth', { value: 700, configurable: true, writable: true })
try {
- render()
+ render()
openList()
const menu = screen.getByRole('list', { name: zh['list.aria'] })
// 700 - 12 - 440 - 344 = -96: the popover moves left to keep the margin.
@@ -282,7 +225,7 @@ describe('ActivityListAction rows and observation', () => {
const originalWidth = window.innerWidth
Object.defineProperty(window, 'innerWidth', { value: 700, configurable: true, writable: true })
try {
- render()
+ render()
openList()
const menu = screen.getByRole('list', { name: zh['list.aria'] })
// max(12 - 4, min(0, 700 - 12 - 800 - 4)) = 8: clamped at the left margin.
@@ -294,39 +237,34 @@ describe('ActivityListAction rows and observation', () => {
it('ignores other keys while open and closes once the roster empties', () => {
const settledPair = [
- row({ id: 'bash-1' as ActivityId, status: 'completed', finishedAt: 2 }),
- row({ id: 'bash-2' as ActivityId, status: 'completed', finishedAt: 3 }),
+ recordJob({ id: 'bash-1' as SessionJob['id'], status: 'completed', finishedAt: 2 }),
+ recordJob({ id: 'bash-2' as SessionJob['id'], status: 'completed', finishedAt: 3 }),
]
- const { rerender } = render()
- expect(screen.getByRole('button', { name: '2 个任务' })).toBeDefined()
+ const { rerender } = render()
+ expect(screen.getByRole('button', { name: '2 个后台任务' })).toBeDefined()
openList()
const list = screen.getByRole('list', { name: zh['list.aria'] })
fireEvent.keyDown(list, { key: 'a' })
expect(screen.getByRole('list', { name: zh['list.aria'] })).toBeDefined()
// The roster emptying unmounts the control entirely; the open flag resets first.
- rerender()
+ rerender()
expect(screen.queryByRole('button')).toBeNull()
})
- it('starts observing on expand, renders the live text, and stops on collapse', () => {
+ it('starts observing on expand with the session identity, renders the live text, and stops on collapse', () => {
const stop = vi.fn()
const observe = vi.fn(() => stop)
- const view: ObservedActivity = {
- activityId: 'bash-1' as ActivityId,
+ const view: ObservedJob = {
+ jobId: 'bash-1' as SessionJob['id'],
text: 'compiling…\n',
gapBefore: false,
status: 'running',
streaming: true,
}
- render()
+ render()
openList()
fireEvent.click(screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') }))
- expect(observe).toHaveBeenCalledWith('bash-1')
+ expect(observe).toHaveBeenCalledWith(SESSION, 'bash-1')
expect(screen.getByText('compiling…')).toBeDefined()
fireEvent.click(screen.getByRole('button', { name: zh['row.collapseAria'].replace('{label}', 'pnpm run build') }))
@@ -334,18 +272,15 @@ describe('ActivityListAction rows and observation', () => {
})
it('surfaces retention gaps and stream failures above the panel', () => {
- const view: ObservedActivity = {
- activityId: 'bash-1' as ActivityId,
+ const view: ObservedJob = {
+ jobId: 'bash-1' as SessionJob['id'],
text: 'tail only',
gapBefore: true,
status: 'running',
streaming: false,
error: 'connection lost',
}
- render()
+ render()
openList()
fireEvent.click(screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') }))
expect(screen.getByText(zh['output.gap'])).toBeDefined()
@@ -354,9 +289,7 @@ describe('ActivityListAction rows and observation', () => {
it('stops observation when the popover closes via Escape', () => {
const stop = vi.fn()
- render( stop)} />)
+ render( stop)} />)
openList()
fireEvent.click(screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') }))
fireEvent.keyDown(screen.getByRole('list', { name: zh['list.aria'] }), { key: 'Escape' })
@@ -368,13 +301,10 @@ describe('ActivityListAction rows and observation', () => {
const stop = vi.fn()
// One stable observe identity across renders, as the inject face provides.
const observe = () => stop
- const first = props({ rowsBySession: { [SESSION]: [row()] } }, observe)
- const { rerender } = render()
+ const { rerender } = render()
openList()
fireEvent.click(screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') }))
- rerender()
+ rerender()
expect(stop).toHaveBeenCalledTimes(1)
expect(screen.queryByRole('button', { name: zh['row.collapseAria'].replace('{label}', 'other') })).toBeNull()
})
diff --git a/packages/client/ui-activity/tsconfig.json b/packages/client/ui-jobs/tsconfig.json
similarity index 89%
rename from packages/client/ui-activity/tsconfig.json
rename to packages/client/ui-jobs/tsconfig.json
index 562e2c2228..4b8aae7886 100644
--- a/packages/client/ui-activity/tsconfig.json
+++ b/packages/client/ui-jobs/tsconfig.json
@@ -10,7 +10,6 @@
"references": [
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
- { "path": "../../api/activity-controller/tsconfig.client.json" },
{ "path": "../../api/session-controller/tsconfig.client.json" },
{ "path": "../locale" },
{ "path": "../ui-conversation" },
diff --git a/packages/client/ui-activity/tsdown.config.ts b/packages/client/ui-jobs/tsdown.config.ts
similarity index 76%
rename from packages/client/ui-activity/tsdown.config.ts
rename to packages/client/ui-jobs/tsdown.config.ts
index 096771fe90..c2f3a27162 100644
--- a/packages/client/ui-activity/tsdown.config.ts
+++ b/packages/client/ui-jobs/tsdown.config.ts
@@ -1,6 +1,6 @@
import { clientBundle } from '../tsdown.client.ts'
export default clientBundle(
- '@deepseek-ai/dsh-client-ui-activity',
+ '@deepseek-ai/dsh-client-ui-jobs',
['lib/types/index.js', 'lib/types/invariant.js'],
)
diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
index 066e930d6d..23b36a8440 100644
--- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
+++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
@@ -1144,8 +1144,8 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
slotInject: '',
declaredBy: 'an entry in \'conversation.session.header\' (client-ui-conversation), so it exists while that entry is mounted',
occupants: [
- 'client-ui-activity ActivityListAction id \'activity-list\'',
'client-ui-agent-preset AgentPresetLabel id \'agent-preset\'',
+ 'client-ui-jobs JobListAction id \'job-list\'',
'client-ui-schedule ScheduleCatalogAction id \'schedule-catalog\'',
'client-ui-subagent SubagentCatalogAction id \'subagent-catalog\'',
'experimental-client-ui-agent-team TeamAction id \'agent-team\'',
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index 21e7f10221..80cc02f219 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/packages/extensions/tool-cordis/src/api-catalog.ts
@@ -81,68 +81,6 @@ export interface TypeApiEntry {
/** Every harness `ctx.` service, sorted by key. */
export const SERVICE_API: readonly ServiceApiEntry[] = [
- {
- key: 'activities',
- summary: 'Abstract streaming-output registry.',
- description: 'Abstract streaming-output registry. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.activities` (one implementation per context; loading a second throws, which is cordis\' standard duplicate-service behavior).\n\nImplementations must honor these semantics:\n\n- Observation never consumes. Any number of readers hold their own absolute byte offsets; a read changes no cursor and no producer state, so the model-facing paths (`ctx.jobs.read()`, tool results) are unaffected.\n- Records outlive producer fibers. Owner disposal ends still-open records and removes them; service disposal ends and clears everything. Neither awaits a producer — the registry owns no execution resource.\n- Retention is bounded. Appends past the live cap drop the oldest retained bytes; a reader below the retained window gets a lossy read, never an error. Settlement trims retention to the settled cap.\n- Listener delivery is owner-relative: a listener registered from an unscoped context — a host composition\'s own carrier — sees every owner, while one registered under an agent composition\'s scope sees exactly the agents composed under it. Every listener is contained.',
- methods: [
- {
- signature: 'abstract open(spec: ActivityOpen): ActivityHandle',
- description: 'Validate the spec, attach owner cleanup, and atomically register one activity. A rejection leaves no id; after return the producer owns the handle and the visible set has changed.',
- parameters: [{ name: 'spec', description: 'activity identity, owner, and correlation.' }],
- returns: 'the producer face of the registered activity.',
- },
- {
- signature: 'abstract list(caller?: Agent): ActivitySnapshot[]',
- description: 'List caller-owned and unowned activities in registration order without exposing another session\'s labels or output.',
- parameters: [{ name: 'caller', description: 'reading agent; a non-agent caller sees only unowned activities.' }],
- returns: 'fresh snapshots.',
- },
- {
- signature: 'abstract get(id: ActivityId, caller?: Agent): ActivitySnapshot',
- description: 'Return a fresh snapshot. Throws for an unknown or foreign activity.',
- parameters: [{ name: 'id', description: 'activity to look up.' }, { name: 'caller', description: 'reading agent checked against the owner.' }],
- returns: 'a fresh snapshot.',
- },
- {
- signature: 'abstract read(id: ActivityId, from: number, caller?: Agent): ActivityRead',
- description: 'Read retained output from an absolute byte offset without consuming it. Resume by passing a previous read\'s `next`; a foreign offset inside a retained chunk returns that whole chunk (its `at` may precede `from`). Throws for an unknown or foreign activity or a negative or non-integer offset.',
- parameters: [{ name: 'id', description: 'activity to read.' }, { name: 'from', description: 'absolute byte offset to read from (0 for the retained head).' }, { name: 'caller', description: 'reading agent checked against the owner.' }],
- returns: 'retained chunks overlapping `[from, total)`, the resume offset, and the lossy flag.',
- },
- {
- signature: 'abstract onActivitiesChanged(listener: ActivitiesChangedListener): () => void',
- description: 'Register an effect-scoped observer of visible-set changes: opening, detail updates, settlement, owner-disposal removal, and the emptying that service disposal commits — so an observer re-reads rather than accumulating deltas. Listeners are contained and never awaited.',
- parameters: [{ name: 'listener', description: 'receives the owner whose visible set changed, or `undefined` when an unowned activity changed and every caller\'s set did.' }],
- returns: 'disposer that unregisters the listener.',
- },
- {
- signature: 'abstract onOutput(listener: ActivityOutputListener): () => void',
- description: 'Register an effect-scoped observer of stream advancement — one signal per committed append and one at settlement, carrying only the activity id. A consumer schedules a read from its own cursor; the registry never pushes payloads. Listeners are contained and never awaited.',
- parameters: [{ name: 'listener', description: 'receives the id whose stream advanced.' }],
- returns: 'disposer that unregisters the listener.',
- },
- ],
- },
- {
- key: 'activityController',
- summary: 'Host service backing the generated `ctx.remote.activity` namespace.',
- description: 'Host service backing the generated `ctx.remote.activity` namespace.',
- methods: [
- {
- signature: '@Remote({ mode: \'stream\' }) control(signal: AbortSignal): AsyncIterable',
- description: 'Stream a complete roster baseline followed by whole-bucket replacements.',
- parameters: [{ name: 'signal', description: 'generation cancellation.' }],
- returns: 'baseline followed by per-owner roster replacement frames.',
- },
- {
- signature: '@Remote({ mode: \'stream\' }) observe(request: ActivityObserveRequest, signal: AbortSignal): AsyncIterable',
- description: 'Stream one activity\'s retained output from an absolute byte offset, then its terminal status once settled and drained. Non-consuming: the model-facing cursors never observe these reads.',
- parameters: [{ name: 'request', description: 'target activity and optional resume offset.' }, { name: 'signal', description: 'generation cancellation.' }],
- returns: 'anchor, coalesced output frames, and the terminal status.',
- },
- ],
- },
{
key: 'agentDefaultModel',
summary: 'Owns the default model selection independently of any Host or transport.',
@@ -1098,7 +1036,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
{
key: 'jobs',
summary: 'Abstract background job registry.',
- description: 'Abstract background job registry. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.jobs` (one implementation per context; loading a second throws, which is cordis\' standard duplicate-service behavior).\n\nImplementations must honor these semantics:\n\n- Registrations outlive producer and controller fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. Teardown cancellation also marks the record reported, because a record its owner is being destroyed for has no reader left.\n- Owned-job access is fenced by the owner\'s session id. Ids are predictable, so authorization — not secrecy — is the boundary.\n- Settlement is first-wins: one terminal record, released waiters, and one round of contained listener notification, even against a late producer outcome. Completion is announced last, after the record is committed and every other observer of the settlement has seen it, because a reporter may open a model turn synchronously.\n- start refuses work while no attached job controller serves the spec\'s owner, so a producer cannot start work that owner cannot collect or stop. One registry serves every composition in the process, so this question — and completion-listener delivery — is owner-relative rather than process-wide: registrations made from an unscoped context serve every owner, and registrations made under an agent composition\'s scope serve exactly the agents composed under it.',
+ description: 'Abstract background job registry. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.jobs` (one implementation per context; loading a second throws, which is cordis\' standard duplicate-service behavior).\n\nImplementations must honor these semantics:\n\n- Registrations outlive producer and controller fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. Teardown cancellation also marks the record reported, because a record its owner is being destroyed for has no reader left.\n- Owned-job access is fenced by the owner\'s session id. Ids are predictable, so authorization — not secrecy — is the boundary.\n- Settlement is first-wins: one terminal record, released waiters, and one round of contained listener notification, even against a late producer outcome. Completion is announced last, after the record is committed and every other observer of the settlement has seen it, because a reporter may open a model turn synchronously.\n- start refuses work while no attached job controller serves the spec\'s owner, so a producer cannot start work that owner cannot collect or stop. One registry serves every composition in the process, so this question — and completion-listener delivery — is owner-relative rather than process-wide: registrations made from an unscoped context serve every owner, and registrations made under an agent composition\'s scope serve exactly the agents composed under it.\n- Record observation never consumes. Any number of readRecord readers hold their own absolute byte offsets; a record read changes no cursor and no notice state, so the model-facing surfaces (the consuming read, completion notices) are unaffected.\n- Record retention is bounded. Appends past the live cap drop the oldest retained bytes; a reader below the retained window gets a lossy read, never an error. Settlement — the producer outcome, a kill, or teardown — trims retention to the settled cap and ends the stream; the record has no separate lifecycle.',
methods: [
{
signature: 'abstract start(spec: JobStart): JobId',
@@ -1148,6 +1086,18 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
parameters: [{ name: 'listener', description: 'receives the owner whose visible set changed, or `undefined` when an unowned job changed and every caller\'s set did.' }],
returns: 'disposer that unregisters the listener.',
},
+ {
+ signature: 'abstract readRecord(id: JobId, from: number, caller?: Agent): JobRecordRead',
+ description: 'Read retained record output from an absolute byte offset without consuming it. Resume by passing a previous read\'s `next`; a foreign offset inside a retained chunk returns that whole chunk (its `at` may precede `from`). Never marks the job reported. Throws for an unknown or foreign job, a job without a JobStart.record declaration, or a negative or non-integer offset.',
+ parameters: [{ name: 'id', description: 'job to read.' }, { name: 'from', description: 'absolute byte offset to read from (0 for the retained head).' }, { name: 'caller', description: 'reading agent checked against the owner.' }],
+ returns: 'retained chunks overlapping `[from, total)`, the resume offset, and the lossy flag.',
+ },
+ {
+ signature: 'abstract onOutput(listener: JobOutputListener): () => void',
+ description: 'Register an effect-scoped observer of record advancement — one signal per committed append and one at settlement, carrying only the job id. A consumer schedules a readRecord from its own cursor; the registry never pushes payloads. Delivery is owner-relative on the same terms as onJobsChanged. Listeners are contained and never awaited.',
+ parameters: [{ name: 'listener', description: 'receives the id whose record advanced.' }],
+ returns: 'disposer that unregisters the listener.',
+ },
{
signature: 'abstract attachController(name: string): () => void',
description: 'Attach an effect-scoped controller that can read and stop jobs. It serves the owners its registering context\'s scope covers, and start refuses an owner no attached controller serves.',
@@ -1505,6 +1455,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
parameters: [{ name: 'signal', description: 'cancellation owned by the Remote stream carrier.' }],
returns: 'one complete baseline followed by live replacement frames.',
},
+ {
+ signature: '@Remote({ mode: \'stream\' }) observeJob(request: SessionObserveJobRequest, signal: AbortSignal): AsyncIterable',
+ description: 'Stream one job\'s retained record output from an absolute byte offset, then its terminal status once settled and drained. Non-consuming: the model-facing cursor and notice state never observe these reads. The request\'s session resolves the fenced-read caller; the registry rejects a job the session does not own, a job without a record, or an unknown job.',
+ parameters: [{ name: 'request', description: 'target job, owning session, and optional resume offset.' }, { name: 'signal', description: 'cancellation owned by the Remote stream carrier.' }],
+ returns: 'anchor, coalesced output frames, and the terminal status.',
+ },
],
},
{
@@ -3473,94 +3429,6 @@ export const EVENT_API: readonly EventApiEntry[] = [
/** Shapes of every exported type the Service and Event signatures reference (transitively), sorted by name. */
export const TYPE_API: readonly TypeApiEntry[] = [
- {
- name: 'ActivitiesChangedListener',
- declaration: 'export type ActivitiesChangedListener = (owner: Agent | undefined) => void;',
- },
- {
- name: 'ActivityAppendOptions',
- declaration: 'export interface ActivityAppendOptions {\n channel?: ActivityChannel;\n gapBefore?: true;\n}',
- },
- {
- name: 'ActivityChannel',
- declaration: 'export type ActivityChannel = \'stdout\' | \'stderr\';',
- },
- {
- name: 'ActivityControlFrame',
- declaration: 'export type ActivityControlFrame = {\n readonly type: \'baseline\';\n readonly activities: readonly ActivityRow[];\n} | {\n readonly type: \'rows\';\n readonly sessionId?: SessionId;\n readonly activities: readonly ActivityRow[];\n};',
- },
- {
- name: 'ActivityCorrelation',
- declaration: 'export interface ActivityCorrelation {\n callId?: ToolCallId;\n jobId?: JobId;\n}',
- },
- {
- name: 'ActivityHandle',
- declaration: 'export interface ActivityHandle {\n readonly id: ActivityId;\n append(text: string, options?: ActivityAppendOptions): void;\n updateDetail(detail: string): void;\n end(outcome: ActivityOutcome): void;\n}',
- },
- {
- name: 'ActivityId',
- declaration: 'export type ActivityId = Branded<\'ActivityId\'>;',
- },
- {
- name: 'ActivityKind',
- declaration: 'export type ActivityKind = ActivityKindMap[keyof ActivityKindMap];',
- },
- {
- name: 'ActivityKindMap',
- declaration: 'export interface ActivityKindMap {\n bash: \'bash\';\n}',
- },
- {
- name: 'ActivityObserveFrame',
- declaration: 'export type ActivityObserveFrame = {\n readonly type: \'opened\';\n readonly activityId: ActivityId;\n readonly from: number;\n readonly earliest: number;\n readonly total: number;\n readonly status: ActivityRowStatus;\n readonly detail?: string;\n} | {\n readonly type: \'output\';\n readonly chunks: readonly ActivityWireChunk[];\n readonly next: number;\n readonly lossy?: true;\n} | {\n readonly type: \'status\';\n readonly status: ActivityRowStatus;\n readonly detail?: string;\n readonly finishedAt?: number;\n};',
- },
- {
- name: 'ActivityObserveRequest',
- declaration: 'export interface ActivityObserveRequest {\n readonly activityId: ActivityId;\n readonly from?: number;\n}',
- },
- {
- name: 'ActivityOpen',
- declaration: 'export interface ActivityOpen {\n kind: ActivityKind;\n label: string;\n owner?: Agent;\n correlation?: ActivityCorrelation;\n}',
- },
- {
- name: 'ActivityOutcome',
- declaration: 'export interface ActivityOutcome {\n status: \'completed\' | \'failed\' | \'killed\';\n detail?: string;\n}',
- },
- {
- name: 'ActivityOutputChunk',
- declaration: 'export interface ActivityOutputChunk {\n at: number;\n text: string;\n channel?: ActivityChannel;\n gapBefore?: true;\n}',
- },
- {
- name: 'ActivityOutputListener',
- declaration: 'export type ActivityOutputListener = (id: ActivityId) => void;',
- },
- {
- name: 'ActivityRead',
- declaration: 'export interface ActivityRead {\n chunks: readonly ActivityOutputChunk[];\n next: number;\n lossy: boolean;\n}',
- },
- {
- name: 'ActivityRow',
- declaration: 'export interface ActivityRow {\n readonly id: ActivityId;\n readonly kind: string;\n readonly label: string;\n readonly sessionId?: SessionId;\n readonly correlation?: ActivityRowCorrelation;\n readonly status: ActivityRowStatus;\n readonly detail?: string;\n readonly startedAt: number;\n readonly finishedAt?: number;\n readonly outputTotal: number;\n}',
- },
- {
- name: 'ActivityRowCorrelation',
- declaration: 'export interface ActivityRowCorrelation {\n readonly callId?: ToolCallId;\n readonly jobId?: JobId;\n}',
- },
- {
- name: 'ActivityRowStatus',
- declaration: 'export type ActivityRowStatus = \'running\' | \'completed\' | \'failed\' | \'killed\';',
- },
- {
- name: 'ActivitySnapshot',
- declaration: 'export interface ActivitySnapshot {\n id: ActivityId;\n kind: ActivityKind;\n label: string;\n ownerSession?: SessionId;\n correlation?: ActivityCorrelation;\n status: ActivityStatus;\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n outputTotal: number;\n outputEarliest: number;\n}',
- },
- {
- name: 'ActivityStatus',
- declaration: 'export type ActivityStatus = \'running\' | \'completed\' | \'failed\' | \'killed\';',
- },
- {
- name: 'ActivityWireChunk',
- declaration: 'export interface ActivityWireChunk {\n readonly at: number;\n readonly text: string;\n readonly channel?: string;\n readonly gapBefore?: true;\n}',
- },
{
name: 'AdapterRegistrationHandle',
declaration: 'export interface AdapterRegistrationHandle {\n (): void;\n replace(providers: string[]): void;\n}',
@@ -4349,6 +4217,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'InvokeRemoteRequest',
declaration: 'export interface InvokeRemoteRequest {\n readonly namespace: string;\n readonly method: string;\n readonly args: Readonly>;\n readonly signal?: AbortSignal;\n}',
},
+ {
+ name: 'JobAppendOptions',
+ declaration: 'export interface JobAppendOptions {\n channel?: JobChannel;\n gapBefore?: true;\n}',
+ },
+ {
+ name: 'JobChannel',
+ declaration: 'export type JobChannel = \'stdout\' | \'stderr\';',
+ },
{
name: 'JobDoneListener',
declaration: 'export type JobDoneListener = (snapshot: JobSnapshot, owner: Agent | undefined) => void | PromiseLike;',
@@ -4373,21 +4249,33 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'JobOutcome',
declaration: 'export interface JobOutcome {\n status: \'completed\' | \'killed\' | \'failed\';\n detail?: string;\n output?: string;\n}',
},
+ {
+ name: 'JobOutputListener',
+ declaration: 'export type JobOutputListener = (id: JobId) => void;',
+ },
{
name: 'JobRead',
declaration: 'export interface JobRead {\n text: string;\n snapshot: JobSnapshot;\n}',
},
+ {
+ name: 'JobRecordChunk',
+ declaration: 'export interface JobRecordChunk {\n at: number;\n text: string;\n channel?: JobChannel;\n gapBefore?: true;\n}',
+ },
+ {
+ name: 'JobRecordRead',
+ declaration: 'export interface JobRecordRead {\n chunks: readonly JobRecordChunk[];\n next: number;\n lossy: boolean;\n}',
+ },
{
name: 'JobsChangedListener',
declaration: 'export type JobsChangedListener = (owner: Agent | undefined) => void;',
},
{
name: 'JobSnapshot',
- declaration: 'export interface JobSnapshot {\n id: JobId;\n kind: JobKind;\n label: string;\n outputLimitBytes?: number;\n ownerSession?: SessionId;\n status: JobStatus;\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n reported: boolean;\n}',
+ declaration: 'export interface JobSnapshot {\n id: JobId;\n kind: JobKind;\n label: string;\n outputLimitBytes?: number;\n ownerSession?: SessionId;\n status: JobStatus;\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n reported: boolean;\n outputTotal?: number;\n outputEarliest?: number;\n}',
},
{
name: 'JobStart',
- declaration: 'export interface JobStart {\n kind: JobKind;\n label: string;\n outputLimitBytes?: number;\n owner?: Agent;\n run(): JobHooks;\n}',
+ declaration: 'export interface JobStart {\n kind: JobKind;\n label: string;\n outputLimitBytes?: number;\n owner?: Agent;\n record?: true;\n run(job: RunningJob): JobHooks;\n}',
},
{
name: 'JobStatus',
@@ -4877,6 +4765,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'RunnerFailureRule',
declaration: 'export interface RunnerFailureRule {\n allowedExitCodes?: readonly number[];\n fatalSignatures: readonly string[];\n informationalLines?: readonly string[];\n}',
},
+ {
+ name: 'RunningJob',
+ declaration: 'export interface RunningJob {\n readonly id: JobId;\n append(text: string, options?: JobAppendOptions): void;\n updateDetail(detail: string): void;\n}',
+ },
{
name: 'SandboxEnforcement',
declaration: 'export type SandboxEnforcement = \'full\' | \'partial\';',
@@ -5103,7 +4995,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SessionJob',
- declaration: 'export interface SessionJob {\n readonly id: JobId;\n readonly kind: string;\n readonly label: string;\n readonly status: \'running\' | \'stopping\' | \'completed\' | \'killed\' | \'failed\';\n readonly detail?: string;\n readonly startedAt: number;\n readonly finishedAt?: number;\n}',
+ declaration: 'export interface SessionJob {\n readonly id: JobId;\n readonly kind: string;\n readonly label: string;\n readonly status: \'running\' | \'stopping\' | \'completed\' | \'killed\' | \'failed\';\n readonly detail?: string;\n readonly startedAt: number;\n readonly finishedAt?: number;\n readonly outputTotal?: number;\n}',
+ },
+ {
+ name: 'SessionJobWireChunk',
+ declaration: 'export interface SessionJobWireChunk {\n readonly at: number;\n readonly text: string;\n readonly channel?: string;\n readonly gapBefore?: true;\n}',
},
{
name: 'SessionLineageNode',
@@ -5137,6 +5033,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'SessionObservationOptions',
declaration: 'export interface SessionObservationOptions {\n readonly signal?: AbortSignal;\n readonly projectionMode?: \'all\' | \'none\';\n}',
},
+ {
+ name: 'SessionObserveJobFrame',
+ declaration: 'export type SessionObserveJobFrame = {\n readonly type: \'opened\';\n readonly jobId: JobId;\n readonly from: number;\n readonly earliest: number;\n readonly total: number;\n readonly status: SessionJob[\'status\'];\n readonly detail?: string;\n} | {\n readonly type: \'output\';\n readonly chunks: readonly SessionJobWireChunk[];\n readonly next: number;\n readonly lossy?: true;\n} | {\n readonly type: \'status\';\n readonly status: SessionJob[\'status\'];\n readonly detail?: string;\n readonly finishedAt?: number;\n};',
+ },
+ {
+ name: 'SessionObserveJobRequest',
+ declaration: 'export interface SessionObserveJobRequest {\n readonly sessionId?: SessionId;\n readonly jobId: JobId;\n readonly from?: number;\n}',
+ },
{
name: 'SessionOpenWorkspacePathRequest',
declaration: 'export interface SessionOpenWorkspacePathRequest {\n readonly path: string;\n}',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index da5b3091d6..d938573ee5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -625,110 +625,6 @@ importers:
specifier: workspace:^
version: link:../../interaction/user-approval
- packages/activity/activity:
- devDependencies:
- '@deepseek-ai/cordis':
- specifier: workspace:^
- version: link:../../../vendor/cordis
- '@deepseek-ai/dsh-agent':
- specifier: workspace:^
- version: link:../../core/agent
- '@deepseek-ai/dsh-brand':
- specifier: workspace:^
- version: link:../../util/brand
- '@deepseek-ai/dsh-invariants':
- specifier: workspace:^
- version: link:../../runtime-diagnostics/invariants
- '@deepseek-ai/dsh-jobs':
- specifier: workspace:^
- version: link:../../jobs/jobs
- '@deepseek-ai/dsh-llm':
- specifier: workspace:^
- version: link:../../llm/llm
- '@deepseek-ai/dsh-session':
- specifier: workspace:^
- version: link:../../core/session
-
- packages/activity/activity-local:
- dependencies:
- '@deepseek-ai/schemastery':
- specifier: link:../../../vendor/schemastery
- version: link:../../../vendor/schemastery
- devDependencies:
- '@deepseek-ai/cordis':
- specifier: workspace:^
- version: link:../../../vendor/cordis
- '@deepseek-ai/cordis-plugin-include':
- specifier: workspace:^
- version: link:../../../vendor/include
- '@deepseek-ai/cordis-plugin-loader':
- specifier: workspace:^
- version: link:../../../vendor/loader
- '@deepseek-ai/dsh-activity':
- specifier: workspace:^
- version: link:../activity
- '@deepseek-ai/dsh-agent':
- specifier: workspace:^
- version: link:../../core/agent
- '@deepseek-ai/dsh-brand':
- specifier: workspace:^
- version: link:../../util/brand
- '@deepseek-ai/dsh-invariants':
- specifier: workspace:^
- version: link:../../runtime-diagnostics/invariants
- '@deepseek-ai/dsh-scope':
- specifier: workspace:^
- version: link:../../core/scope
- '@deepseek-ai/dsh-session':
- specifier: workspace:^
- version: link:../../core/session
-
- packages/api/activity-controller:
- dependencies:
- '@deepseek-ai/schemastery':
- specifier: link:../../../vendor/schemastery
- version: link:../../../vendor/schemastery
- zod:
- specifier: ^4.4.3
- version: 4.4.3
- devDependencies:
- '@deepseek-ai/cordis':
- specifier: workspace:^
- version: link:../../../vendor/cordis
- '@deepseek-ai/dsh-activity':
- specifier: workspace:^
- version: link:../../activity/activity
- '@deepseek-ai/dsh-activity-local':
- specifier: workspace:^
- version: link:../../activity/activity-local
- '@deepseek-ai/dsh-agent':
- specifier: workspace:^
- version: link:../../core/agent
- '@deepseek-ai/dsh-api-gateway':
- specifier: workspace:^
- version: link:../gateway
- '@deepseek-ai/dsh-client-connection':
- specifier: workspace:^
- version: link:../../client/connection
- '@deepseek-ai/dsh-client-store':
- specifier: workspace:^
- version: link:../../client/store
- '@deepseek-ai/dsh-invariants':
- specifier: workspace:^
- version: link:../../runtime-diagnostics/invariants
- '@deepseek-ai/dsh-jobs':
- specifier: workspace:^
- version: link:../../jobs/jobs
- '@deepseek-ai/dsh-llm':
- specifier: workspace:^
- version: link:../../llm/llm
- '@deepseek-ai/dsh-session':
- specifier: workspace:^
- version: link:../../core/session
- '@deepseek-ai/dsh-typert-protocol':
- specifier: workspace:^
- version: link:../../typert/protocol
-
packages/api/gateway:
dependencies:
'@deepseek-ai/dsh-deque':
@@ -793,9 +689,6 @@ importers:
'@deepseek-ai/dsh-agent-presets':
specifier: workspace:^
version: link:../../preset/agent-presets
- '@deepseek-ai/dsh-api-activity-controller':
- specifier: workspace:^
- version: link:../activity-controller
'@deepseek-ai/dsh-api-gateway':
specifier: workspace:^
version: link:../gateway
@@ -1174,9 +1067,6 @@ importers:
'@deepseek-ai/cordis-plugin-timer':
specifier: workspace:^
version: link:../../../vendor/timer
- '@deepseek-ai/dsh-activity-local':
- specifier: workspace:^
- version: link:../../activity/activity-local
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
@@ -1571,9 +1461,6 @@ importers:
'@deepseek-ai/dsh-agent-presets':
specifier: workspace:^
version: link:../../preset/agent-presets
- '@deepseek-ai/dsh-api-activity-controller':
- specifier: workspace:^
- version: link:../../api/activity-controller
'@deepseek-ai/dsh-api-remotes':
specifier: workspace:^
version: link:../../api/remotes
@@ -1601,9 +1488,6 @@ importers:
'@deepseek-ai/dsh-client-modules':
specifier: workspace:^
version: link:../../client/modules
- '@deepseek-ai/dsh-client-ui-activity':
- specifier: workspace:^
- version: link:../../client/ui-activity
'@deepseek-ai/dsh-client-ui-agent-preset':
specifier: workspace:^
version: link:../../client/ui-agent-preset
@@ -1643,6 +1527,9 @@ importers:
'@deepseek-ai/dsh-client-ui-input-trigger':
specifier: workspace:^
version: link:../../client/ui-input-trigger
+ '@deepseek-ai/dsh-client-ui-jobs':
+ specifier: workspace:^
+ version: link:../../client/ui-jobs
'@deepseek-ai/dsh-client-ui-layout':
specifier: workspace:^
version: link:../../client/ui-layout
@@ -1961,48 +1848,6 @@ importers:
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
- packages/client/ui-activity:
- devDependencies:
- '@deepseek-ai/cordis':
- specifier: workspace:^
- version: link:../../../vendor/cordis
- '@deepseek-ai/dsh-api-activity-controller':
- specifier: workspace:^
- version: link:../../api/activity-controller
- '@deepseek-ai/dsh-api-session-controller':
- specifier: workspace:^
- version: link:../../api/session-controller
- '@deepseek-ai/dsh-client-locale':
- specifier: workspace:^
- version: link:../locale
- '@deepseek-ai/dsh-client-test-runtime':
- specifier: workspace:^
- version: link:../../test-support/client-runtime
- '@deepseek-ai/dsh-client-ui-conversation':
- specifier: workspace:^
- version: link:../ui-conversation
- '@deepseek-ai/dsh-client-ui-primitives':
- specifier: workspace:^
- version: link:../ui-primitives
- '@deepseek-ai/dsh-client-ui-renderer':
- specifier: workspace:^
- version: link:../ui-renderer
- '@deepseek-ai/dsh-client-ui-session':
- specifier: workspace:^
- version: link:../ui-session
- '@deepseek-ai/dsh-client-ui-slots':
- specifier: workspace:^
- version: link:../ui-slots
- '@deepseek-ai/dsh-invariants':
- specifier: workspace:^
- version: link:../../runtime-diagnostics/invariants
- '@types/react':
- specifier: ~18.3.1
- version: 18.3.31
- react:
- specifier: ^18.2.0
- version: 18.3.1
-
packages/client/ui-agent-preset:
devDependencies:
'@deepseek-ai/cordis':
@@ -2723,6 +2568,48 @@ importers:
specifier: ^18.2.0
version: 18.3.1
+ packages/client/ui-jobs:
+ devDependencies:
+ '@deepseek-ai/cordis':
+ specifier: workspace:^
+ version: link:../../../vendor/cordis
+ '@deepseek-ai/dsh-api-session-controller':
+ specifier: workspace:^
+ version: link:../../api/session-controller
+ '@deepseek-ai/dsh-client-locale':
+ specifier: workspace:^
+ version: link:../locale
+ '@deepseek-ai/dsh-client-test-runtime':
+ specifier: workspace:^
+ version: link:../../test-support/client-runtime
+ '@deepseek-ai/dsh-client-ui-conversation':
+ specifier: workspace:^
+ version: link:../ui-conversation
+ '@deepseek-ai/dsh-client-ui-primitives':
+ specifier: workspace:^
+ version: link:../ui-primitives
+ '@deepseek-ai/dsh-client-ui-renderer':
+ specifier: workspace:^
+ version: link:../ui-renderer
+ '@deepseek-ai/dsh-client-ui-session':
+ specifier: workspace:^
+ version: link:../ui-session
+ '@deepseek-ai/dsh-client-ui-slots':
+ specifier: workspace:^
+ version: link:../ui-slots
+ '@deepseek-ai/dsh-invariants':
+ specifier: workspace:^
+ version: link:../../runtime-diagnostics/invariants
+ '@deepseek-ai/dsh-session':
+ specifier: workspace:^
+ version: link:../../core/session
+ '@types/react':
+ specifier: ~18.3.1
+ version: 18.3.31
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+
packages/client/ui-layout:
devDependencies:
'@deepseek-ai/cordis':
@@ -8288,9 +8175,6 @@ importers:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
- '@deepseek-ai/dsh-activity':
- specifier: workspace:^
- version: link:../../activity/activity
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
@@ -8416,9 +8300,6 @@ importers:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
- '@deepseek-ai/dsh-activity':
- specifier: workspace:^
- version: link:../../activity/activity
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
@@ -10405,12 +10286,6 @@ importers:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
- '@deepseek-ai/dsh-activity':
- specifier: workspace:^
- version: link:../../activity/activity
- '@deepseek-ai/dsh-activity-local':
- specifier: workspace:^
- version: link:../../activity/activity-local
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
@@ -10575,9 +10450,6 @@ importers:
'@deepseek-ai/dsh-acp':
specifier: workspace:^
version: link:../../packages/acp/acp
- '@deepseek-ai/dsh-activity':
- specifier: workspace:^
- version: link:../../packages/activity/activity
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../packages/core/agent
diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json
index 2ae2320a08..f735144468 100644
--- a/python/sdk-runtime/package.json
+++ b/python/sdk-runtime/package.json
@@ -13,7 +13,6 @@
"@deepseek-ai/cosmokit": "workspace:^",
"@deepseek-ai/dsh": "workspace:^",
"@deepseek-ai/dsh-acp": "workspace:^",
- "@deepseek-ai/dsh-activity": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-instructions": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts
index 02eccc59d1..d477494d34 100644
--- a/scripts/gen-cordis-catalog.ts
+++ b/scripts/gen-cordis-catalog.ts
@@ -109,8 +109,6 @@ export const SERVICE_PAGE: Record = {
subprocess: 'subprocess.md',
systemPrompt: 'system-prompt.md',
jobs: 'jobs.md',
- activities: 'activity.md',
- activityController: 'activity.md',
sessionTelemetry: 'session-telemetry.md',
agentTeams: 'agent-team.md',
tokenMeter: 'token-meter.md',
@@ -175,7 +173,7 @@ export const SERVICE_WALK_EXEMPTIONS: Record = {
slots: 'client-side interface-typed browser service — packages/client/ui-renderer/README.md owns the API',
theme: 'client-side interface-typed browser service — packages/client/ui-theme/README.md owns the API',
workspaces: 'client-side interface-typed browser service — packages/api/workspace-controller/README.md owns the API',
- activityFeed: 'client-side interface-typed browser service — packages/api/activity-controller/README.md owns the API',
+ jobOutput: 'client-side interface-typed browser service — packages/api/session-controller/README.md owns the API',
}
/**
@@ -322,6 +320,8 @@ export const LINK_MAP: Readonly> = {
SessionId: 'core.md',
SessionListRequest: 'session.md',
SessionListValue: 'session.md',
+ SessionObserveJobFrame: 'session.md',
+ SessionObserveJobRequest: 'session.md',
ModelCatalog: 'session.md',
SessionOpenWorkspacePathRequest: 'session.md',
SessionOpenWorkspacePathValue: 'session.md',
@@ -501,22 +501,18 @@ export const LINK_MAP: Readonly> = {
PromptSectionOrderName: 'system-prompt.md',
SystemPrompt: 'system-prompt.md',
ToolProviderResult: 'system-prompt.md',
- ActivitiesChangedListener: 'activity.md',
- ActivityHandle: 'activity.md',
- ActivityId: 'activity.md',
- ActivityOpen: 'activity.md',
- ActivityOutputListener: 'activity.md',
- ActivityRead: 'activity.md',
- ActivitySnapshot: 'activity.md',
- ActivityControlFrame: 'activity.md',
- ActivityObserveFrame: 'activity.md',
- ActivityObserveRequest: 'activity.md',
JobDoneListener: 'jobs.md',
JobId: 'jobs.md',
JobRead: 'jobs.md',
JobSnapshot: 'jobs.md',
JobStart: 'jobs.md',
JobsChangedListener: 'jobs.md',
+ JobAppendOptions: 'jobs.md',
+ JobChannel: 'jobs.md',
+ JobOutputListener: 'jobs.md',
+ JobRecordChunk: 'jobs.md',
+ JobRecordRead: 'jobs.md',
+ RunningJob: 'jobs.md',
CreateTeamTaskRequest: 'agent-team.md',
SendTeamMessageRequest: 'agent-team.md',
SendTeamMessageResult: 'agent-team.md',
diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts
index 008991a00c..3638b0c002 100644
--- a/scripts/gen-doc-graphs.ts
+++ b/scripts/gen-doc-graphs.ts
@@ -198,13 +198,6 @@ const SERVICE_ROLES: ServiceRole[] = [
mode: 'core',
note: 'Carries the picking seam onto the wire: capability gating, cancellation, and the seam-coded failures a browser directory flow discriminates on.',
},
- {
- key: 'activityController',
- pkg: 'api-activity-controller',
- title: 'Host Activity Remote controller',
- mode: 'core',
- note: 'Owns the live-activity roster stream and per-activity non-consuming output observation through the generated Remote namespace.',
- },
{
key: 'invariants',
pkg: 'invariants',
@@ -583,17 +576,8 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Background job registry',
mode: 'seam',
implementations: ['jobs-local'],
- consumers: ['tool-bash', 'tool-terminal', 'tool-subagent', 'tool-jobs'],
- note: 'Producers (background bash, PTY sends, and subagent delegations) register running work; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry.',
- },
- {
- key: 'activities',
- pkg: 'activity',
- title: 'Live activity observation registry',
- mode: 'seam',
- implementations: ['activity-local'],
- consumers: ['tool-bash', 'tool-pwsh', 'tool-workflow', 'activity-controller'],
- note: 'Producers mirror live output and status into non-consuming offset-addressed streams; activity-controller serves them to browsers, and the model-facing paths (jobs cursors, tool results) stay untouched.',
+ consumers: ['tool-bash', 'tool-pwsh', 'tool-terminal', 'tool-subagent', 'tool-jobs'],
+ note: 'Producers (background bash/pwsh, PTY sends, and subagent delegations) register running work; record-declaring jobs additionally stream raw output for non-consuming observers; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry.',
},
{
key: 'web',
diff --git a/scripts/package-dependency-policy.ts b/scripts/package-dependency-policy.ts
index 15b481383c..63054fd183 100644
--- a/scripts/package-dependency-policy.ts
+++ b/scripts/package-dependency-policy.ts
@@ -5,7 +5,6 @@ const CLIENT_FACE_INCLUDE: readonly string[] = []
/** Packages exempted from automatic Client/Host treatment despite declaring `dsh.client`. */
const CLIENT_FACE_EXCLUDE: readonly string[] = [
- '@deepseek-ai/dsh-api-activity-controller',
'@deepseek-ai/dsh-api-session-controller',
'@deepseek-ai/dsh-api-workspace-controller',
]
diff --git a/scripts/verify-package-dependencies.spec.ts b/scripts/verify-package-dependencies.spec.ts
index 26263fe018..4294cff84b 100644
--- a/scripts/verify-package-dependencies.spec.ts
+++ b/scripts/verify-package-dependencies.spec.ts
@@ -123,7 +123,6 @@ function hostRuntimeFixture(): {
describe('package dependency scope', () => {
it('keeps the measured Host relay roster explicit', () => {
expect(PACKAGE_DEPENDENCY_POLICY.clientFaceExclude).toEqual([
- '@deepseek-ai/dsh-api-activity-controller',
'@deepseek-ai/dsh-api-session-controller',
'@deepseek-ai/dsh-api-workspace-controller',
])
diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts
index ec942708ea..a563dc46bd 100644
--- a/scripts/verify-package-readme-model-experience.ts
+++ b/scripts/verify-package-readme-model-experience.ts
@@ -46,10 +46,7 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly> = {
const SENTENCE_MODEL_EXPERIENCE: Readonly> = {
'packages/attachment/attachment': { kind: 'indirect', reason: 'The storage seam delegates model request rendering to provider adapters.' },
'packages/attachment/attachment-local': { kind: 'indirect', reason: 'The local backend delegates model request rendering to provider adapters.' },
- 'packages/activity/activity': { kind: 'none', reason: 'Pure observation seam for humans; activity output becomes model-visible only through producers\' existing tool results and jobs reads.' },
- 'packages/activity/activity-local': { kind: 'none', reason: 'In-memory observation provider; producers own every model-visible surface.' },
- 'packages/api/activity-controller': { kind: 'none', reason: 'Browser/Host observation transport; registers no prompt, tool, or session event.' },
- 'packages/client/ui-activity': { kind: 'none', reason: 'Browser-side presentation of host-observed output; touches no prompt, message, schema, stream, or tool result.' },
+ 'packages/client/ui-jobs': { kind: 'none', reason: 'Browser-side presentation of host-observed output; touches no prompt, message, schema, stream, or tool result.' },
'packages/shell/shell': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },
'packages/shell/shell-env': { kind: 'indirect', reason: 'The env service exposes managed DSH_* facts through the shell tools (dsh-tool-bash/dsh-tool-pwsh); it registers no prompt or schema of its own.' },
'packages/shell/bash-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-bash.' },
diff --git a/tsconfig.base.json b/tsconfig.base.json
index c09254eec2..d8ba3b6830 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -47,13 +47,6 @@
"@deepseek-ai/dsh-api-gateway/client": ["./packages/api/gateway/src/client/index.ts"],
"@deepseek-ai/dsh-api-gateway/invariant": ["./packages/api/gateway/src/invariant.ts"],
"@deepseek-ai/dsh-api-gateway/types": ["./packages/api/gateway/src/types.ts"],
- "@deepseek-ai/dsh-api-activity-controller": ["./packages/api/activity-controller/src/index.ts"],
- "@deepseek-ai/dsh-api-activity-controller/client": ["./packages/api/activity-controller/src/client/index.ts"],
- "@deepseek-ai/dsh-api-activity-controller/invariant": ["./packages/api/activity-controller/src/invariant.ts"],
- "@deepseek-ai/dsh-api-activity-controller/types": ["./packages/api/activity-controller/src/types.ts"],
- "@deepseek-ai/dsh-client-ui-activity": ["./packages/client/ui-activity/src"],
- "@deepseek-ai/dsh-client-ui-activity/client": ["./packages/client/ui-activity/src/client/index.ts"],
- "@deepseek-ai/dsh-client-ui-activity/invariant": ["./packages/client/ui-activity/src/invariant.ts"],
"@deepseek-ai/dsh-api-session-controller": ["./packages/api/session-controller/src/index.ts"],
"@deepseek-ai/dsh-api-session-controller/client": ["./packages/api/session-controller/src/client/index.ts"],
"@deepseek-ai/dsh-api-session-controller/invariant": ["./packages/api/session-controller/src/invariant.ts"],
@@ -108,7 +101,6 @@
"@deepseek-ai/dsh-commands/brand": ["./packages/interaction/commands/src/brand.ts"],
"@deepseek-ai/dsh-commands/types": ["./packages/interaction/commands/src/types.ts"],
"@deepseek-ai/dsh-jobs/brand": ["./packages/jobs/jobs/src/brand.ts"],
- "@deepseek-ai/dsh-activity/brand": ["./packages/activity/activity/src/brand.ts"],
"@deepseek-ai/dsh-credentials/types": ["./packages/credentials/credentials/src/types.ts"],
"@deepseek-ai/dsh-authorization/types": ["./packages/credentials/authorization/src/types.ts"],
"@deepseek-ai/dsh-settings/types": ["./packages/settings/settings/src/types.ts"],
@@ -212,6 +204,8 @@
"@deepseek-ai/dsh-experimental-client-ui-agent-team": ["./packages/experimental/client-ui-agent-team/src"],
"@deepseek-ai/dsh-client-ui-settings-plugins": ["./packages/client/ui-settings-plugins/src"],
"@deepseek-ai/dsh-client-ui-jobs": ["./packages/client/ui-jobs/src"],
+ "@deepseek-ai/dsh-client-ui-jobs/client": ["./packages/client/ui-jobs/src/client/index.ts"],
+ "@deepseek-ai/dsh-client-ui-jobs/invariant": ["./packages/client/ui-jobs/src/invariant.ts"],
"@deepseek-ai/dsh-client-ui-schedule": ["./packages/client/ui-schedule/src"],
"@deepseek-ai/dsh-client-ui-schedule/client": ["./packages/client/ui-schedule/src/client/index.ts"],
"@deepseek-ai/dsh-client-ui-schedule/invariant": ["./packages/client/ui-schedule/src/invariant.ts"],
@@ -255,10 +249,6 @@
"@deepseek-ai/dsh-acp/invariant": ["./packages/acp/acp/src/invariant.ts"],
"@deepseek-ai/dsh-acp-app": ["./packages/bundle/acp-app/src"],
"@deepseek-ai/dsh-acp-app/invariant": ["./packages/bundle/acp-app/src/invariant.ts"],
- "@deepseek-ai/dsh-activity": ["./packages/activity/activity/src"],
- "@deepseek-ai/dsh-activity/invariant": ["./packages/activity/activity/src/invariant.ts"],
- "@deepseek-ai/dsh-activity-local": ["./packages/activity/activity-local/src"],
- "@deepseek-ai/dsh-activity-local/invariant": ["./packages/activity/activity-local/src/invariant.ts"],
"@deepseek-ai/dsh-agent": ["./packages/core/agent/src"],
"@deepseek-ai/dsh-agent-default-model": ["./packages/core/agent-default-model/src"],
"@deepseek-ai/dsh-agent-default-model/invariant": ["./packages/core/agent-default-model/src/invariant.ts"],
diff --git a/tsconfig.client.json b/tsconfig.client.json
index 2426473861..bb6735bbc7 100644
--- a/tsconfig.client.json
+++ b/tsconfig.client.json
@@ -57,7 +57,6 @@
{ "path": "./packages/typert/registry" },
{ "path": "./packages/api/gateway/tsconfig.client.json" },
{ "path": "./packages/api/session-controller/tsconfig.client.json" },
- { "path": "./packages/api/activity-controller/tsconfig.client.json" },
{ "path": "./packages/api/workspace-controller/tsconfig.client.json" },
{ "path": "./packages/api/remotes/tsconfig.client.json" },
{ "path": "./packages/extensions/cordis-client-runner" },
@@ -78,7 +77,7 @@
{ "path": "./packages/client/ui-skill" },
{ "path": "./packages/client/ui-reference" },
{ "path": "./packages/client/ui-subagent" },
- { "path": "./packages/client/ui-activity" },
+ { "path": "./packages/client/ui-jobs" },
{ "path": "./packages/experimental/client-ui-agent-team" },
{ "path": "./packages/client/ui-schedule" },
{ "path": "./packages/client/ui-directory-picker-browse" },
diff --git a/tsconfig.host.json b/tsconfig.host.json
index b9fac3cdff..4bbbda019b 100644
--- a/tsconfig.host.json
+++ b/tsconfig.host.json
@@ -81,7 +81,7 @@
"apps/web/tests/subagent-interrupt-ui.e2e.ts",
"apps/web/tests/sidebar-subagent-activity.e2e.ts",
"apps/web/tests/background-job-list.e2e.ts",
- "apps/web/tests/live-activity-stream.e2e.ts",
+ "apps/web/tests/live-job-stream.e2e.ts",
"apps/web/tests/bash-abort-row.e2e.ts",
"apps/web/tests/skill-tool-row.e2e.ts",
"apps/web/tests/turn-tail-actions.e2e.ts",
@@ -152,7 +152,6 @@
{ "path": "./packages/api/gateway/tsconfig.host.json" },
{ "path": "./packages/api/remotes/tsconfig.host.json" },
{ "path": "./packages/api/session-controller/tsconfig.host.json" },
- { "path": "./packages/api/activity-controller/tsconfig.host.json" },
{ "path": "./packages/api/settings-controller" },
{ "path": "./packages/api/workspace-controller/tsconfig.host.json" },
{ "path": "./packages/typert/loader" },
@@ -298,8 +297,6 @@
{ "path": "./packages/jobs/jobs" },
{ "path": "./packages/jobs/jobs-local" },
{ "path": "./packages/jobs/tool-jobs" },
- { "path": "./packages/activity/activity" },
- { "path": "./packages/activity/activity-local" },
{ "path": "./packages/experimental/agent-team" },
{ "path": "./packages/experimental/agent-team-profile" },
{ "path": "./packages/experimental/tool-agent-team" },