Merge feat/creation-recommendations into plugin recommendation experiment
Takes the card packages' new names and master up to e115cc98d65. The plugin recommendation packages and their bundle rows now name @deepseek-ai/dsh-action-cards and @deepseek-ai/dsh-client-ui-action-cards; generated catalogs are regenerated, and both card Web fixtures record the renamed bundles in their skills' plugin catalogs.
This commit is contained in:
commit
30dafd2106
168 files changed
+3094
-620
No files matched your search
@@ -108,7 +108,7 @@
|
||||
"@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*",
|
||||
"@deepseek-ai/dsh-cua-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-plugin-market-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-creation-recommendations": "workspace:*",
|
||||
"@deepseek-ai/dsh-creation-recommendations": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-plugin-recommendations": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// Browser hit testing for the conversation view tabs: each tab accepts presses
|
||||
// beyond its label, and the selected tab's bar stays above the Trajectory
|
||||
// view's sticky toolbar where the two meet.
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { createChatScrollFixture } from './chat-scroll-fixture.ts'
|
||||
import { launchWebScaffold, seedSession, watchConsole, type WebScaffold } from './scaffold.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const FIXTURE = createChatScrollFixture({
|
||||
markerPrefix: 'VIEW_TABS',
|
||||
title: 'VIEW_TABS short session',
|
||||
turns: 4,
|
||||
})
|
||||
const SEED_ID = 'conversation-view-tabs-web-e2e'
|
||||
|
||||
/** Label and button rectangles of one tab, and which element owns each probe point. */
|
||||
interface TabProbe {
|
||||
readonly label: { left: number; top: number; right: number; bottom: number }
|
||||
readonly button: { left: number; top: number; right: number; bottom: number }
|
||||
readonly ownsAboveLabel: boolean
|
||||
readonly ownsBesideLabel: boolean
|
||||
readonly ownsBarOverhang: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe one tab: the run above and beside its label, and the bar's lowest
|
||||
* half pixel, below the button box.
|
||||
* @param page - the page under test.
|
||||
* @param name - the tab's accessible name.
|
||||
* @returns the tab's rectangles and probe owners.
|
||||
*/
|
||||
async function probeTab(page: Page, name: string): Promise<TabProbe> {
|
||||
return page.getByRole('tab', { name, exact: true }).evaluate((tab) => {
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(tab)
|
||||
const label = range.getBoundingClientRect()
|
||||
const button = tab.getBoundingClientRect()
|
||||
const owns = (x: number, y: number): boolean => document.elementFromPoint(x, y)?.closest('[role="tab"]') === tab
|
||||
const centerX = (label.left + label.right) / 2
|
||||
return {
|
||||
label: { left: label.left, top: label.top, right: label.right, bottom: label.bottom },
|
||||
button: { left: button.left, top: button.top, right: button.right, bottom: button.bottom },
|
||||
ownsAboveLabel: owns(centerX, label.top - 8),
|
||||
ownsBesideLabel: owns(label.left - 8, (label.top + label.bottom) / 2),
|
||||
ownsBarOverhang: owns(centerX, button.bottom + 0.5),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function openSeededSession(page: Page): Promise<void> {
|
||||
const searchButton = page.getByRole('button', { name: 'Search sessions' })
|
||||
if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
|
||||
await page.getByRole('textbox', { name: 'Search session names', exact: true }).fill(FIXTURE.markers.user(1))
|
||||
const results = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem')
|
||||
const deadline = Date.now() + 60_000
|
||||
while (await results.count() !== 1) {
|
||||
if (Date.now() > deadline) throw new Error('seeded session never appeared in the sidebar search results')
|
||||
await page.waitForTimeout(200)
|
||||
}
|
||||
await results.click()
|
||||
}
|
||||
|
||||
describe('web e2e: conversation view tabs', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
await seedSession(scaffold, FIXTURE.log, SEED_ID)
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser, 900)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await openSeededSession(page)
|
||||
await page.getByText(FIXTURE.markers.assistant(FIXTURE.turns), { exact: false }).last()
|
||||
.waitFor({ timeout: 30_000 })
|
||||
}, 180_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('takes presses above and beside the label', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-conversation-view-tabs-target'))
|
||||
const chat = await probeTab(page, 'Chat')
|
||||
expect(chat.button.top).toBeLessThan(chat.label.top - 8)
|
||||
expect(chat.button.left).toBeLessThan(chat.label.left - 8)
|
||||
expect(chat.ownsAboveLabel).toBe(true)
|
||||
expect(chat.ownsBesideLabel).toBe(true)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps the selected bar above the Trajectory toolbar', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-conversation-view-tabs-trajectory'))
|
||||
await page.getByRole('tab', { name: 'Trajectory', exact: true }).click()
|
||||
const toolbar = page.getByRole('toolbar', { name: 'Trajectory toolbar' })
|
||||
await toolbar.waitFor({ timeout: 30_000 })
|
||||
const trajectory = await probeTab(page, 'Trajectory')
|
||||
// The toolbar must start within the bar's overhang, or the probe cannot fail.
|
||||
expect((await toolbar.boundingBox())!.y).toBeLessThanOrEqual(trajectory.button.bottom + 1)
|
||||
expect(trajectory.ownsBarOverhang).toBe(true)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -863,7 +863,7 @@ describe('web e2e: plugin manager', () => {
|
||||
['@deepseek-ai/dsh-experimental-voice-input-bundle', 1],
|
||||
['@deepseek-ai/dsh-experimental-plugin-market-bundle', 1],
|
||||
['@deepseek-ai/dsh-experimental-auto-review', 1],
|
||||
['@deepseek-ai/dsh-experimental-creation-recommendations', 1],
|
||||
['@deepseek-ai/dsh-creation-recommendations', 1],
|
||||
['@deepseek-ai/dsh-experimental-plugin-recommendations', 1],
|
||||
['@deepseek-ai/dsh-experimental-inspector-profile', 1],
|
||||
['@deepseek-ai/dsh-cua-bundle', 1],
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { ContextFormed, GenerateOptions, StreamChunk } from '@deepseek-ai/d
|
||||
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
ScheduleId,
|
||||
createAfterScheduleRecord,
|
||||
createEveryScheduleRecord,
|
||||
foldScheduleEvents,
|
||||
type EveryScheduleRecord,
|
||||
@@ -1259,4 +1260,86 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
|
||||
await page.clock.setFixedTime(new Date(CATALOG_NOW))
|
||||
}
|
||||
}, 60_000)
|
||||
|
||||
it('removes every ended task from the page and keeps the active ones', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-clear-ended'))
|
||||
const endedSession = SessionId('schedule-manager-ended-clear')
|
||||
const domain = scaffold.ctx.storageDomain.get('schedule')
|
||||
if (domain === undefined) throw new Error('Schedule domain was not opened')
|
||||
const endedIds = [ScheduleId('schedule-ended-first'), ScheduleId('schedule-ended-second')]
|
||||
const endedTitles = ['Ended release check', 'Ended metrics check'] as const
|
||||
// An earlier scenario may leave an ended row behind; the confirmation states
|
||||
// the whole ended set, so the lane starts from an empty one.
|
||||
await scaffold.ctx.schedule.deleteInactive()
|
||||
for (const [index, id] of endedIds.entries()) {
|
||||
const title = endedTitles[index]!
|
||||
const record = createAfterScheduleRecord(id, title, 60, Date.now(), title)
|
||||
const receipt = {
|
||||
scheduledAt: record.scheduledAt,
|
||||
deliveredAt: new Date(Date.parse(record.scheduledAt) + 1_000).toISOString(),
|
||||
messageId: MessageId(`${id}-delivery`),
|
||||
}
|
||||
await domain.table('tasks').put(record.id, {
|
||||
sessionId: endedSession, record, status: 'inactive', lastDelivery: receipt,
|
||||
deliveryHistory: { records: [receipt], earlierRecordsUnavailable: false },
|
||||
})
|
||||
}
|
||||
// The running task is what the cleanup must leave alone. Creating it also
|
||||
// announces a committed Host change, so the open page reads the rows seeded
|
||||
// above without waiting for its own first catalog read.
|
||||
const kept = await scaffold.ctx.schedule.create(endedSession, {
|
||||
prompt: 'Keep this running task', title: 'Keep this running task', at: '2099-08-25T12:08:00.000Z',
|
||||
})
|
||||
try {
|
||||
await page.getByRole('button', { name: 'Automation tasks', exact: true }).click()
|
||||
const manager = page.getByTestId('task-manager-page')
|
||||
// Earlier scenarios leave their own search text and status filter on this
|
||||
// shared page; this scenario asserts the complete list.
|
||||
await manager.getByRole('searchbox', { name: 'Search tasks' }).fill('')
|
||||
await manager.getByRole('button', { name: 'All', exact: true }).click()
|
||||
const more = manager.getByRole('button', { name: 'More task actions', exact: true })
|
||||
await more.waitFor({ timeout: 15_000 })
|
||||
|
||||
// The confirmation states the Host-side target and removes nothing until it
|
||||
// is confirmed.
|
||||
await more.click()
|
||||
await page.getByRole('menuitem', { name: 'Clear all inactive tasks', exact: true }).click()
|
||||
const confirmation = page.getByRole('dialog', { name: 'Clear 2 inactive tasks?' })
|
||||
await confirmation.getByRole('button', { name: 'Cancel', exact: true }).click()
|
||||
expect((await scaffold.ctx.schedule.catalog()).filter(task => task.status === 'inactive')).toHaveLength(2)
|
||||
|
||||
await more.click()
|
||||
await page.getByRole('menuitem', { name: 'Clear all inactive tasks', exact: true }).click()
|
||||
await confirmation.getByRole('button', { name: 'Clear tasks', exact: true }).click()
|
||||
await expect.poll(
|
||||
async () => (await scaffold.ctx.schedule.catalog()).filter(task => task.status === 'inactive'),
|
||||
{ timeout: 15_000 },
|
||||
).toEqual([])
|
||||
const remaining = await scaffold.ctx.schedule.catalog()
|
||||
expect(remaining.some(task => endedIds.includes(task.id))).toBe(false)
|
||||
expect(remaining.map(task => task.title)).toContain('Keep this running task')
|
||||
await page.getByText('Cleared 2 inactive tasks.', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
// The trigger left with the rows it removed, so the page heading owns the
|
||||
// focus after the confirmation closes; this confirms the heading fallback
|
||||
// in a real browser.
|
||||
await expect.poll(() => page.evaluate(() => document.activeElement?.tagName), { timeout: 15_000 })
|
||||
.toBe('H1')
|
||||
// The page follows the acknowledged removal: no ended row or cleanup action
|
||||
// remains, and the running task is still listed.
|
||||
await expect.poll(() => manager.getByRole('button', { name: 'More task actions', exact: true }).count(), { timeout: 15_000 })
|
||||
.toBe(0)
|
||||
await manager.getByRole('button', { name: 'All', exact: true }).click()
|
||||
await expect.poll(() => manager.getByRole('list', { name: 'Task catalog' })
|
||||
.getByRole('button', { name: kept.title, exact: true }).count(), { timeout: 15_000 }).toBe(1)
|
||||
expect((await scaffold.ctx.schedule.catalog()).filter(task => task.status === 'inactive')).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
} finally {
|
||||
// The shared page and Host outlive a failed assertion: remove every row
|
||||
// this scenario introduced so the next one starts from the same state. A
|
||||
// teardown rejection must not mask the assertion that failed first.
|
||||
await Promise.allSettled(
|
||||
[kept.id, ...endedIds].map(id => scaffold.ctx.schedule.delete({ sessionId: endedSession, id })),
|
||||
)
|
||||
}
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
// Web e2e scenario: a composer statistics pill cut off by the narrow composer
|
||||
// row shows its full reading in a tooltip on hover. The seed is generated, not recorded: no line is model output.
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { createSystemMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SEED_ID = 'stats-truncated-tooltip-web-e2e'
|
||||
const USAGE_READING = '5.8M tok · Cache hit 98%'
|
||||
|
||||
/**
|
||||
* Generate one closed single-step turn whose billed usage totals 5.8M tokens at a 98% cache hit.
|
||||
* @returns session.jsonl text for {@link seedSession}.
|
||||
*/
|
||||
function buildSeed(): string {
|
||||
const lines = [JSON.stringify({
|
||||
type: 'session', version: SESSION_FORMAT_VERSION, id: '{{sessionId}}',
|
||||
createdAt: 1784974100000, cwd: '{{cwd}}/workspace', isSeeded: false, delegationDepth: 0,
|
||||
})]
|
||||
let seq = 0
|
||||
let time = 1784974100000
|
||||
const at = (event: Record<string, unknown>): void => {
|
||||
lines.push(JSON.stringify({ ...event, seq: seq++, time: time++ }))
|
||||
}
|
||||
at({ type: 'turn/start', data: { turn: 1 } })
|
||||
at({ type: 'step/start', data: { turn: 1, step: 1 } })
|
||||
at({ type: 'system/message', data: { turn: 1, step: 1, message: createSystemMessage('') }, surfaceOp: 'append' })
|
||||
at({
|
||||
type: 'user/message',
|
||||
data: {
|
||||
id: '00000000-0000-4000-9000-000000000001',
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'm1' }],
|
||||
source: { kind: 'user' },
|
||||
},
|
||||
surfaceOp: 'append',
|
||||
})
|
||||
at({
|
||||
type: 'assistant/message',
|
||||
data: {
|
||||
stream: [],
|
||||
turn: 1,
|
||||
step: 1,
|
||||
message: {
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'r1' }],
|
||||
source: { kind: 'model', provider: 'snapshot', model: 'snapshot-replier' },
|
||||
},
|
||||
usage: { inputTokens: 100_000, outputTokens: 20_000, cacheReadTokens: 5_680_000 },
|
||||
},
|
||||
surfaceOp: 'append',
|
||||
})
|
||||
at({ type: 'step/end', data: { turn: 1, step: 1 } })
|
||||
at({ type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } })
|
||||
return `${lines.join('\n')}\n`
|
||||
}
|
||||
|
||||
describe('web e2e: truncated composer statistics', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
if (webSnapshotMode() === 'record') throw new Error('stats-truncated-tooltip is a keyless assembled scenario')
|
||||
scaffold = await launchWebScaffold({})
|
||||
await seedSession(scaffold, buildSeed(), SEED_ID)
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('shows the full reading of a pill the narrow composer cuts off', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-stats-truncated-tooltip'))
|
||||
await page.locator('[role="treeitem"]').first().click()
|
||||
await page.locator('[role="treeitem"]').nth(1).click()
|
||||
const usagePill = page.getByRole('button', { name: USAGE_READING, exact: true })
|
||||
await usagePill.waitFor({ timeout: 15_000 })
|
||||
const tooltip = page.getByRole('tooltip')
|
||||
const label = usagePill.locator(':scope > span')
|
||||
const cut = () => label.evaluate(element => element.scrollWidth > element.clientWidth)
|
||||
expect(await cut()).toBe(false)
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 900 })
|
||||
await expect.poll(cut).toBe(true)
|
||||
await usagePill.hover()
|
||||
await expect.poll(() => tooltip.textContent()).toBe(USAGE_READING)
|
||||
await usagePill.click()
|
||||
await page.getByRole('dialog', { name: 'Token usage' }).waitFor()
|
||||
expect(await tooltip.count()).toBe(0)
|
||||
}, 60_000)
|
||||
|
||||
it('stayed clean', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -374,7 +374,9 @@ describe('web e2e: macOS window drag coverage', () => {
|
||||
{
|
||||
row: 'conversation header',
|
||||
what: 'the blank run right of the view tabs, on the tab strip row',
|
||||
at: () => [tabs.x + tabs.width + 8, tabs.y + tabs.height / 2],
|
||||
// The last tab's press target overhangs the tablist by 10px and the
|
||||
// More launcher starts 20px past it; probe the run between them.
|
||||
at: () => [tabs.x + tabs.width + 15, tabs.y + tabs.height / 2],
|
||||
drag: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"tests/preview-boot.e2e.ts",
|
||||
"tests/seeded-history.e2e.ts",
|
||||
"tests/stats-paged-history.e2e.ts",
|
||||
"tests/stats-truncated-tooltip.e2e.ts",
|
||||
"tests/sidebar-scrollbar.e2e.ts",
|
||||
"tests/sidebar-title-hover-scroll.e2e.ts",
|
||||
"tests/rail-search-expand.e2e.ts",
|
||||
@@ -224,6 +225,7 @@
|
||||
"tests/chat-long-interactions.e2e.ts",
|
||||
"tests/chat-continuous-conversation.e2e.ts",
|
||||
"tests/composer-tab-geometry.e2e.ts",
|
||||
"tests/conversation-view-tabs.e2e.ts",
|
||||
"tests/window-drag-coverage.e2e.ts",
|
||||
"tests/complex-history.perf.ts",
|
||||
"tests/pwsh-terminal.e2e.ts",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
en: 41d772192075e133
|
||||
zh: 5ef3da69b4de1d8a
|
||||
/capability-seams-and-core-services:
|
||||
en: a65a5f6deaadd8b3
|
||||
zh: a412bd0fccd52090
|
||||
en: 78a7164902e62d31
|
||||
zh: 109232bf6430cb6e
|
||||
@@ -7,10 +7,10 @@ A service can be a core spine service, a swappable capability seam, a bundle/com
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
pkg_experimental_action_cards["experimental-action-cards"]
|
||||
pkg_action_cards["action-cards"]
|
||||
svc_actionCards["ctx.actionCards<br/>Optional action cards"]
|
||||
pkg_experimental_creation_recommendations["experimental-creation-recommendations"]
|
||||
pkg_experimental_client_ui_action_cards["experimental-client-ui-action-cards"]
|
||||
pkg_creation_recommendations["creation-recommendations"]
|
||||
pkg_client_ui_action_cards["client-ui-action-cards"]
|
||||
pkg_experimental_plugin_recommendations["experimental-plugin-recommendations"]
|
||||
svc_pluginRecommendations["ctx.pluginRecommendations<br/>Explicit-need plugin recommendations"]
|
||||
pkg_experimental_client_ui_plugin_recommendations["experimental-client-ui-plugin-recommendations"]
|
||||
@@ -305,6 +305,7 @@ flowchart LR
|
||||
pkg_cordis_host_runner["cordis-host-runner"]
|
||||
svc_dynamicCordisRunner["ctx.dynamicCordisRunner<br/>Dynamic Cordis package host runner"]
|
||||
svc_cordisInspect["ctx.cordisInspect<br/>Dynamic Cordis inspect registry"]
|
||||
pkg_action_cards --> svc_actionCards
|
||||
pkg_agent --> svc_agents
|
||||
pkg_agent_default_model --> svc_agentDefaultModel
|
||||
pkg_agent_loop --> svc_agentLoop
|
||||
@@ -348,7 +349,6 @@ flowchart LR
|
||||
pkg_deepseek_account --> svc_deepseekAccount
|
||||
pkg_deepseek_account_platform --> svc_deepseekAccount
|
||||
pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions
|
||||
pkg_experimental_action_cards --> svc_actionCards
|
||||
pkg_experimental_agent_team --> svc_agentTeams
|
||||
pkg_experimental_api_speech_to_text --> svc_speechController
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse
|
||||
@@ -466,8 +466,8 @@ flowchart LR
|
||||
pkg_working_directory --> svc_workingDirectory
|
||||
pkg_workspace --> svc_workspaceRegistry
|
||||
pkg_workspace_changes --> svc_workspaceChanges
|
||||
svc_actionCards --> pkg_experimental_client_ui_action_cards
|
||||
svc_actionCards --> pkg_experimental_creation_recommendations
|
||||
svc_actionCards --> pkg_client_ui_action_cards
|
||||
svc_actionCards --> pkg_creation_recommendations
|
||||
svc_agentDefaultModel --> pkg_api_session_controller
|
||||
svc_agentDefaultModel --> pkg_headless
|
||||
svc_agentLoop --> pkg_base
|
||||
@@ -631,7 +631,7 @@ flowchart LR
|
||||
|
||||
| ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| `ctx.actionCards` | `service` | [`experimental-action-cards`](../packages/experimental/action-cards) | - | [`experimental-creation-recommendations`](../packages/experimental/creation-recommendations), [`experimental-client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | - | Owns the card tool, card state, user actions and status notices; card kinds register their actions and handlers. |
|
||||
| `ctx.actionCards` | `service` | [`action-cards`](../packages/experimental/action-cards) | - | [`creation-recommendations`](../packages/experimental/creation-recommendations), [`client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | - | Owns the card tool, card state, user actions and status notices; card kinds register their actions and handlers. |
|
||||
| `ctx.pluginRecommendations` | `service` | [`experimental-plugin-recommendations`](../packages/experimental/plugin-recommendations) | - | [`experimental-client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | - | Reads the current catalog text of an official plugin card and enables the plugin through the profile plugin manager after a click. |
|
||||
| `ctx.headlessStartup` | `core` | [`headless`](../packages/bundle/headless) | - | [`headless`](../packages/bundle/headless) | - | Supplies parsed task and run options to the headless runner and its sandbox-policy configuration. |
|
||||
| `ctx.cotTranslation` | `service` | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | - | - | Serves reasoning translations, reuses saved results, and joins ordinary Session activation for uncached inactive Sessions. |
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
pkg_experimental_action_cards["experimental-action-cards"]
|
||||
pkg_action_cards["action-cards"]
|
||||
svc_actionCards["ctx.actionCards<br/>Optional action cards"]
|
||||
pkg_experimental_creation_recommendations["experimental-creation-recommendations"]
|
||||
pkg_experimental_client_ui_action_cards["experimental-client-ui-action-cards"]
|
||||
pkg_creation_recommendations["creation-recommendations"]
|
||||
pkg_client_ui_action_cards["client-ui-action-cards"]
|
||||
pkg_experimental_plugin_recommendations["experimental-plugin-recommendations"]
|
||||
svc_pluginRecommendations["ctx.pluginRecommendations<br/>Explicit-need plugin recommendations"]
|
||||
pkg_experimental_client_ui_plugin_recommendations["experimental-client-ui-plugin-recommendations"]
|
||||
@@ -307,6 +307,7 @@ flowchart LR
|
||||
pkg_cordis_host_runner["cordis-host-runner"]
|
||||
svc_dynamicCordisRunner["ctx.dynamicCordisRunner<br/>Dynamic Cordis package host runner"]
|
||||
svc_cordisInspect["ctx.cordisInspect<br/>Dynamic Cordis inspect registry"]
|
||||
pkg_action_cards --> svc_actionCards
|
||||
pkg_agent --> svc_agents
|
||||
pkg_agent_default_model --> svc_agentDefaultModel
|
||||
pkg_agent_loop --> svc_agentLoop
|
||||
@@ -350,7 +351,6 @@ flowchart LR
|
||||
pkg_deepseek_account --> svc_deepseekAccount
|
||||
pkg_deepseek_account_platform --> svc_deepseekAccount
|
||||
pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions
|
||||
pkg_experimental_action_cards --> svc_actionCards
|
||||
pkg_experimental_agent_team --> svc_agentTeams
|
||||
pkg_experimental_api_speech_to_text --> svc_speechController
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse
|
||||
@@ -468,8 +468,8 @@ flowchart LR
|
||||
pkg_working_directory --> svc_workingDirectory
|
||||
pkg_workspace --> svc_workspaceRegistry
|
||||
pkg_workspace_changes --> svc_workspaceChanges
|
||||
svc_actionCards --> pkg_experimental_client_ui_action_cards
|
||||
svc_actionCards --> pkg_experimental_creation_recommendations
|
||||
svc_actionCards --> pkg_client_ui_action_cards
|
||||
svc_actionCards --> pkg_creation_recommendations
|
||||
svc_agentDefaultModel --> pkg_api_session_controller
|
||||
svc_agentDefaultModel --> pkg_headless
|
||||
svc_agentLoop --> pkg_base
|
||||
@@ -633,7 +633,7 @@ flowchart LR
|
||||
|
||||
| ctx 键 | 角色 | 所属包 | 实现 | 直接消费方 | 配套插件 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| `ctx.actionCards` | `service` | [`experimental-action-cards`](../packages/experimental/action-cards) | - | [`experimental-creation-recommendations`](../packages/experimental/creation-recommendations), [`experimental-client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | - | 负责卡片工具、卡片状态、用户操作和状态通知;各卡片种类注册自己的动作和处理器。 |
|
||||
| `ctx.actionCards` | `service` | [`action-cards`](../packages/experimental/action-cards) | - | [`creation-recommendations`](../packages/experimental/creation-recommendations), [`client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | - | 负责卡片工具、卡片状态、用户操作和状态通知;各卡片种类注册自己的动作和处理器。 |
|
||||
| `ctx.pluginRecommendations` | `service` | [`experimental-plugin-recommendations`](../packages/experimental/plugin-recommendations) | - | [`experimental-client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | - | 读取官方插件卡片的当前目录文本,并在用户点击后通过配置插件管理器启用该插件。 |
|
||||
| `ctx.headlessStartup` | `core` | [`headless`](../packages/bundle/headless) | - | [`headless`](../packages/bundle/headless) | - | 将解析后的任务和运行选项提供给无界面运行器及其 sandbox-policy 配置。 |
|
||||
| `ctx.cotTranslation` | `service` | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | - | - | 为展开的思考内容提供有界的 Session 翻译请求,不改变模型可见历史。 |
|
||||
|
||||
+18
-18
@@ -33,6 +33,21 @@ export interface AcpConfig {
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-acp -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-action-cards -->
|
||||
<a id="deepseek-aidsh-action-cards"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-action-cards`
|
||||
|
||||
- `inject`: `tools` · `agents` · `sessionProjections` · `remoteAccess`
|
||||
- `refs`: `z` (`zod`)
|
||||
- `source`: [`packages/experimental/action-cards/src/index.ts:23`](../packages/experimental/action-cards/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Validated card limits after schema defaults. */
|
||||
export type Config = z.infer<typeof Config>
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-action-cards -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-agent-default-model -->
|
||||
<a id="deepseek-aidsh-agent-default-model"></a>
|
||||
|
||||
@@ -843,21 +858,6 @@ export interface Config {
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-deepseek-account-platform -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-experimental-action-cards -->
|
||||
<a id="deepseek-aidsh-experimental-action-cards"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-experimental-action-cards`
|
||||
|
||||
- `inject`: `tools` · `agents` · `sessionProjections` · `remoteAccess`
|
||||
- `refs`: `z` (`zod`)
|
||||
- `source`: [`packages/experimental/action-cards/src/index.ts:23`](../packages/experimental/action-cards/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Validated card limits after schema defaults. */
|
||||
export type Config = z.infer<typeof Config>
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-experimental-action-cards -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-experimental-agent-team -->
|
||||
<a id="deepseek-aidsh-experimental-agent-team"></a>
|
||||
|
||||
@@ -4704,6 +4704,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
| `@deepseek-ai/dsh-client-locale` | — | [`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-modules` | `loader` | [`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-resources` | — | [`packages/client/resources/src/index.ts`](../packages/client/resources/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-action-cards` | — | [`packages/experimental/client-ui-action-cards/src/index.ts`](../packages/experimental/client-ui-action-cards/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-agent-preset` | — | [`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-approval` | — | [`packages/client/ui-approval/src/index.ts`](../packages/client/ui-approval/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-attachment` | — | [`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts) |
|
||||
@@ -4712,6 +4713,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
| `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) |
|
||||
@@ -4763,18 +4765,16 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
| `@deepseek-ai/dsh-computer-use` | — | [`packages/computer-use/computer-use/src/index.ts`](../packages/computer-use/computer-use/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-config-editor` | `loader` · `profileContext` | [`packages/boot/config-editor/src/index.ts`](../packages/boot/config-editor/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-creation-recommendations` | `tools` · `pluginManager` · `agents` · `subagents` · `profileContext` · `skills` · `actionCards` | [`packages/experimental/creation-recommendations/src/index.ts`](../packages/experimental/creation-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-action-cards` | — | [`packages/experimental/client-ui-action-cards/src/index.ts`](../packages/experimental/client-ui-action-cards/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-plugin-recommendations` | — | [`packages/experimental/client-ui-plugin-recommendations/src/index.ts`](../packages/experimental/client-ui-plugin-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-voice-input` | — | [`packages/experimental/client-ui-voice-input/src/index.ts`](../packages/experimental/client-ui-voice-input/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-computer-use-cua-driver-native` | `computerUse` · `tools` · `systemPrompt` | [`packages/experimental/computer-use-cua-driver-native/src/index.ts`](../packages/experimental/computer-use-cua-driver-native/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-creation-recommendations` | `tools` · `pluginManager` · `agents` · `subagents` · `profileContext` · `skills` · `actionCards` | [`packages/experimental/creation-recommendations/src/index.ts`](../packages/experimental/creation-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-plugin-recommendations` | `pluginManager` · `agents` · `skills` · `actionCards` | [`packages/experimental/plugin-recommendations/src/index.ts`](../packages/experimental/plugin-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-session-inspector` | — | [`packages/experimental/session-inspector/src/index.ts`](../packages/experimental/session-inspector/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-tool-worktree` | `tools` · `worktrees` | [`packages/experimental/tool-worktree/src/index.ts`](../packages/experimental/tool-worktree/src/index.ts) |
|
||||
|
||||
+18
-18
@@ -35,6 +35,21 @@ export interface AcpConfig {
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-acp -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-action-cards -->
|
||||
<a id="deepseek-aidsh-action-cards"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-action-cards`
|
||||
|
||||
- `inject`: `tools` · `agents` · `sessionProjections` · `remoteAccess`
|
||||
- `refs`: `z` (`zod`)
|
||||
- `source`: [`packages/experimental/action-cards/src/index.ts:23`](../packages/experimental/action-cards/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Validated card limits after schema defaults. */
|
||||
export type Config = z.infer<typeof Config>
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-action-cards -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-agent-default-model -->
|
||||
<a id="deepseek-aidsh-agent-default-model"></a>
|
||||
|
||||
@@ -845,21 +860,6 @@ export interface Config {
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-deepseek-account-platform -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-experimental-action-cards -->
|
||||
<a id="deepseek-aidsh-experimental-action-cards"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-experimental-action-cards`
|
||||
|
||||
- `inject`: `tools` · `agents` · `sessionProjections` · `remoteAccess`
|
||||
- `refs`: `z` (`zod`)
|
||||
- `source`: [`packages/experimental/action-cards/src/index.ts:23`](../packages/experimental/action-cards/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Validated card limits after schema defaults. */
|
||||
export type Config = z.infer<typeof Config>
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-experimental-action-cards -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-experimental-agent-team -->
|
||||
<a id="deepseek-aidsh-experimental-agent-team"></a>
|
||||
|
||||
@@ -4706,6 +4706,7 @@ export interface Config {
|
||||
| `@deepseek-ai/dsh-client-locale` | — | [`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-modules` | `loader` | [`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-resources` | — | [`packages/client/resources/src/index.ts`](../packages/client/resources/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-action-cards` | — | [`packages/experimental/client-ui-action-cards/src/index.ts`](../packages/experimental/client-ui-action-cards/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-agent-preset` | — | [`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-approval` | — | [`packages/client/ui-approval/src/index.ts`](../packages/client/ui-approval/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-attachment` | — | [`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts) |
|
||||
@@ -4714,6 +4715,7 @@ export interface Config {
|
||||
| `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) |
|
||||
@@ -4765,18 +4767,16 @@ export interface Config {
|
||||
| `@deepseek-ai/dsh-computer-use` | — | [`packages/computer-use/computer-use/src/index.ts`](../packages/computer-use/computer-use/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-config-editor` | `loader` · `profileContext` | [`packages/boot/config-editor/src/index.ts`](../packages/boot/config-editor/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-creation-recommendations` | `tools` · `pluginManager` · `agents` · `subagents` · `profileContext` · `skills` · `actionCards` | [`packages/experimental/creation-recommendations/src/index.ts`](../packages/experimental/creation-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-action-cards` | — | [`packages/experimental/client-ui-action-cards/src/index.ts`](../packages/experimental/client-ui-action-cards/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-plugin-recommendations` | — | [`packages/experimental/client-ui-plugin-recommendations/src/index.ts`](../packages/experimental/client-ui-plugin-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-voice-input` | — | [`packages/experimental/client-ui-voice-input/src/index.ts`](../packages/experimental/client-ui-voice-input/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-computer-use-cua-driver-native` | `computerUse` · `tools` · `systemPrompt` | [`packages/experimental/computer-use-cua-driver-native/src/index.ts`](../packages/experimental/computer-use-cua-driver-native/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-creation-recommendations` | `tools` · `pluginManager` · `agents` · `subagents` · `profileContext` · `skills` · `actionCards` | [`packages/experimental/creation-recommendations/src/index.ts`](../packages/experimental/creation-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-plugin-recommendations` | `pluginManager` · `agents` · `skills` · `actionCards` | [`packages/experimental/plugin-recommendations/src/index.ts`](../packages/experimental/plugin-recommendations/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-session-inspector` | — | [`packages/experimental/session-inspector/src/index.ts`](../packages/experimental/session-inspector/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-tool-worktree` | `tools` · `worktrees` | [`packages/experimental/tool-worktree/src/index.ts`](../packages/experimental/tool-worktree/src/index.ts) |
|
||||
|
||||
@@ -11,13 +11,13 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, `creation-recommendations`, [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | `action-cards`, [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, `creation-recommendations`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | `action-cards`, [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
@@ -60,7 +60,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:407`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:414`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
|
||||
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
|
||||
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:484`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
|
||||
| `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
@@ -80,7 +80,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | `action-cards`, [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:44`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, `creation-recommendations`, [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | `action-cards`, [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, `creation-recommendations`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | `action-cards`, [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
@@ -62,7 +62,7 @@
|
||||
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:407`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:414`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
|
||||
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
|
||||
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:484`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
|
||||
| `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
@@ -82,7 +82,7 @@
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | `action-cards`, [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:44`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
en: a11f106978c9dce7
|
||||
zh: a11f106978c9dce7
|
||||
/experimental-plugin-record-catalog:
|
||||
en: 292d2b1448650f82
|
||||
zh: c4c7118a82026e12
|
||||
en: cb931db3b4bcbe33
|
||||
zh: aca9cfecfdee34c0
|
||||
@@ -13,6 +13,6 @@ Shared Session event declarations and resolved schemas appear in the [Session pe
|
||||
|
||||
| Record | Owner | Purpose | Payload and source |
|
||||
|---|---|---|---|
|
||||
| `plugin:action-cards/decision` | `@deepseek-ai/dsh-experimental-action-cards` | Dismissals and kind-defined progress of action cards. | <code>ActionCardDecision</code> · [`packages/experimental/action-cards/src/types.ts`](../packages/experimental/action-cards/src/types.ts) |
|
||||
| `plugin:action-cards/decision` | `@deepseek-ai/dsh-action-cards` | Dismissals and kind-defined progress of action cards. | <code>ActionCardDecision</code> · [`packages/experimental/action-cards/src/types.ts`](../packages/experimental/action-cards/src/types.ts) |
|
||||
| `plugin:translator/request` | `@deepseek-ai/dsh-experimental-translator` | Provider-independent translation input, retained before dispatch. | <code>TranslationRequestRecord</code> · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) |
|
||||
| `plugin:translator/result` | `@deepseek-ai/dsh-experimental-translator` | Successful translation, retained before it is returned to the consumer. | <code>TranslationResultRecord</code> · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) |
|
||||
@@ -13,6 +13,6 @@
|
||||
|
||||
| 记录 | 所属包 | 用途 | 载荷与源码 |
|
||||
|---|---|---|---|
|
||||
| `plugin:action-cards/decision` | `@deepseek-ai/dsh-experimental-action-cards` | Dismissals and kind-defined progress of action cards. | <code>ActionCardDecision</code> · [`packages/experimental/action-cards/src/types.ts`](../packages/experimental/action-cards/src/types.ts) |
|
||||
| `plugin:action-cards/decision` | `@deepseek-ai/dsh-action-cards` | Dismissals and kind-defined progress of action cards. | <code>ActionCardDecision</code> · [`packages/experimental/action-cards/src/types.ts`](../packages/experimental/action-cards/src/types.ts) |
|
||||
| `plugin:translator/request` | `@deepseek-ai/dsh-experimental-translator` | Provider-independent translation input, retained before dispatch. | <code>TranslationRequestRecord</code> · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) |
|
||||
| `plugin:translator/result` | `@deepseek-ai/dsh-experimental-translator` | Successful translation, retained before it is returned to the consumer. | <code>TranslationResultRecord</code> · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) |
|
||||
+16
-16
@@ -254,9 +254,12 @@ flowchart TD
|
||||
pkg_office_to_pdf["office-to-pdf"]
|
||||
end
|
||||
subgraph group_experimental["packages/experimental"]
|
||||
pkg_action_cards["action-cards"]
|
||||
pkg_client_ui_action_cards["client-ui-action-cards"]
|
||||
pkg_client_ui_creation_recommendations["client-ui-creation-recommendations"]
|
||||
pkg_creation_recommendations["creation-recommendations"]
|
||||
pkg_cua["cua"]
|
||||
pkg_cua_bundle["cua-bundle"]
|
||||
pkg_experimental_action_cards["experimental-action-cards"]
|
||||
pkg_experimental_agent_team["experimental-agent-team"]
|
||||
pkg_experimental_agent_team_profile["experimental-agent-team-profile"]
|
||||
pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"]
|
||||
@@ -267,17 +270,14 @@ flowchart TD
|
||||
pkg_experimental_browser_use_runtime["experimental-browser-use-runtime"]
|
||||
pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"]
|
||||
pkg_experimental_claude_code_mods["experimental-claude-code-mods"]
|
||||
pkg_experimental_client_ui_action_cards["experimental-client-ui-action-cards"]
|
||||
pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"]
|
||||
pkg_experimental_client_ui_claude_code_mods["experimental-client-ui-claude-code-mods"]
|
||||
pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"]
|
||||
pkg_experimental_client_ui_creation_recommendations["experimental-client-ui-creation-recommendations"]
|
||||
pkg_experimental_client_ui_plugin_recommendations["experimental-client-ui-plugin-recommendations"]
|
||||
pkg_experimental_client_ui_voice_input["experimental-client-ui-voice-input"]
|
||||
pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"]
|
||||
pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"]
|
||||
pkg_experimental_cot_translation_bundle["experimental-cot-translation-bundle"]
|
||||
pkg_experimental_creation_recommendations["experimental-creation-recommendations"]
|
||||
pkg_experimental_cua_macos["experimental-cua-macos"]
|
||||
pkg_experimental_inspector["experimental-inspector"]
|
||||
pkg_experimental_inspector_profile["experimental-inspector-profile"]
|
||||
@@ -933,10 +933,14 @@ flowchart TD
|
||||
pkg_workspace_changes --> pkg_session
|
||||
pkg_workspace_changes --> pkg_subprocess
|
||||
pkg_workspace_changes --> pkg_tools
|
||||
pkg_experimental_action_cards --> pkg_llm
|
||||
pkg_experimental_action_cards --> pkg_session
|
||||
pkg_experimental_action_cards --> pkg_tools
|
||||
pkg_experimental_action_cards --> pkg_typert_protocol
|
||||
pkg_action_cards --> pkg_llm
|
||||
pkg_action_cards --> pkg_session
|
||||
pkg_action_cards --> pkg_tools
|
||||
pkg_action_cards --> pkg_typert_protocol
|
||||
pkg_creation_recommendations --> pkg_llm
|
||||
pkg_creation_recommendations --> pkg_session
|
||||
pkg_creation_recommendations --> pkg_skill
|
||||
pkg_creation_recommendations --> pkg_tools
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_agent
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt
|
||||
@@ -954,10 +958,6 @@ flowchart TD
|
||||
pkg_experimental_computer_use_cua_driver_native --> pkg_computer_use
|
||||
pkg_experimental_computer_use_cua_driver_native --> pkg_system_prompt
|
||||
pkg_experimental_computer_use_cua_driver_native --> pkg_tools
|
||||
pkg_experimental_creation_recommendations --> pkg_llm
|
||||
pkg_experimental_creation_recommendations --> pkg_session
|
||||
pkg_experimental_creation_recommendations --> pkg_skill
|
||||
pkg_experimental_creation_recommendations --> pkg_tools
|
||||
pkg_experimental_cua_macos --> pkg_agent
|
||||
pkg_experimental_cua_macos --> pkg_computer_use
|
||||
pkg_experimental_cua_macos --> pkg_subprocess
|
||||
@@ -1577,13 +1577,13 @@ flowchart TD
|
||||
| [`client-web`](../packages/client/web) | `client` | — |
|
||||
| [`credentials`](../packages/credentials/credentials) | `credentials` | — |
|
||||
| [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — |
|
||||
| [`client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | `experimental` | — |
|
||||
| [`client-ui-creation-recommendations`](../packages/experimental/client-ui-creation-recommendations) | `experimental` | — |
|
||||
| [`cua-bundle`](../packages/experimental/cua-bundle) | `experimental` | — |
|
||||
| [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — |
|
||||
| [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — |
|
||||
| [`experimental-client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | `experimental` | — |
|
||||
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | — |
|
||||
| [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | — |
|
||||
| [`experimental-client-ui-creation-recommendations`](../packages/experimental/client-ui-creation-recommendations) | `experimental` | — |
|
||||
| [`experimental-client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | `experimental` | — |
|
||||
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | — |
|
||||
| [`experimental-cot-translation-bundle`](../packages/experimental/cot-translation-bundle) | `experimental` | — |
|
||||
@@ -1751,12 +1751,12 @@ flowchart TD
|
||||
| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-action-cards`](../packages/experimental/action-cards) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`action-cards`](../packages/experimental/action-cards) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`creation-recommendations`](../packages/experimental/creation-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-creation-recommendations`](../packages/experimental/creation-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-cua-macos`](../packages/experimental/cua-macos) | `experimental` | [`agent`](../packages/core/agent), [`computer-use`](../packages/computer-use/computer-use), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | `experimental` | [`experimental-worktree`](../packages/experimental/worktree), [`tools`](../packages/core/tools) |
|
||||
| [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
|
||||
+16
-16
@@ -256,9 +256,12 @@ flowchart TD
|
||||
pkg_office_to_pdf["office-to-pdf"]
|
||||
end
|
||||
subgraph group_experimental["packages/experimental"]
|
||||
pkg_action_cards["action-cards"]
|
||||
pkg_client_ui_action_cards["client-ui-action-cards"]
|
||||
pkg_client_ui_creation_recommendations["client-ui-creation-recommendations"]
|
||||
pkg_creation_recommendations["creation-recommendations"]
|
||||
pkg_cua["cua"]
|
||||
pkg_cua_bundle["cua-bundle"]
|
||||
pkg_experimental_action_cards["experimental-action-cards"]
|
||||
pkg_experimental_agent_team["experimental-agent-team"]
|
||||
pkg_experimental_agent_team_profile["experimental-agent-team-profile"]
|
||||
pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"]
|
||||
@@ -269,17 +272,14 @@ flowchart TD
|
||||
pkg_experimental_browser_use_runtime["experimental-browser-use-runtime"]
|
||||
pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"]
|
||||
pkg_experimental_claude_code_mods["experimental-claude-code-mods"]
|
||||
pkg_experimental_client_ui_action_cards["experimental-client-ui-action-cards"]
|
||||
pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"]
|
||||
pkg_experimental_client_ui_claude_code_mods["experimental-client-ui-claude-code-mods"]
|
||||
pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"]
|
||||
pkg_experimental_client_ui_creation_recommendations["experimental-client-ui-creation-recommendations"]
|
||||
pkg_experimental_client_ui_plugin_recommendations["experimental-client-ui-plugin-recommendations"]
|
||||
pkg_experimental_client_ui_voice_input["experimental-client-ui-voice-input"]
|
||||
pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"]
|
||||
pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"]
|
||||
pkg_experimental_cot_translation_bundle["experimental-cot-translation-bundle"]
|
||||
pkg_experimental_creation_recommendations["experimental-creation-recommendations"]
|
||||
pkg_experimental_cua_macos["experimental-cua-macos"]
|
||||
pkg_experimental_inspector["experimental-inspector"]
|
||||
pkg_experimental_inspector_profile["experimental-inspector-profile"]
|
||||
@@ -935,10 +935,14 @@ flowchart TD
|
||||
pkg_workspace_changes --> pkg_session
|
||||
pkg_workspace_changes --> pkg_subprocess
|
||||
pkg_workspace_changes --> pkg_tools
|
||||
pkg_experimental_action_cards --> pkg_llm
|
||||
pkg_experimental_action_cards --> pkg_session
|
||||
pkg_experimental_action_cards --> pkg_tools
|
||||
pkg_experimental_action_cards --> pkg_typert_protocol
|
||||
pkg_action_cards --> pkg_llm
|
||||
pkg_action_cards --> pkg_session
|
||||
pkg_action_cards --> pkg_tools
|
||||
pkg_action_cards --> pkg_typert_protocol
|
||||
pkg_creation_recommendations --> pkg_llm
|
||||
pkg_creation_recommendations --> pkg_session
|
||||
pkg_creation_recommendations --> pkg_skill
|
||||
pkg_creation_recommendations --> pkg_tools
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_agent
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt
|
||||
@@ -956,10 +960,6 @@ flowchart TD
|
||||
pkg_experimental_computer_use_cua_driver_native --> pkg_computer_use
|
||||
pkg_experimental_computer_use_cua_driver_native --> pkg_system_prompt
|
||||
pkg_experimental_computer_use_cua_driver_native --> pkg_tools
|
||||
pkg_experimental_creation_recommendations --> pkg_llm
|
||||
pkg_experimental_creation_recommendations --> pkg_session
|
||||
pkg_experimental_creation_recommendations --> pkg_skill
|
||||
pkg_experimental_creation_recommendations --> pkg_tools
|
||||
pkg_experimental_cua_macos --> pkg_agent
|
||||
pkg_experimental_cua_macos --> pkg_computer_use
|
||||
pkg_experimental_cua_macos --> pkg_subprocess
|
||||
@@ -1579,13 +1579,13 @@ flowchart TD
|
||||
| [`client-web`](../packages/client/web) | `client` | — |
|
||||
| [`credentials`](../packages/credentials/credentials) | `credentials` | — |
|
||||
| [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — |
|
||||
| [`client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | `experimental` | — |
|
||||
| [`client-ui-creation-recommendations`](../packages/experimental/client-ui-creation-recommendations) | `experimental` | — |
|
||||
| [`cua-bundle`](../packages/experimental/cua-bundle) | `experimental` | — |
|
||||
| [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — |
|
||||
| [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — |
|
||||
| [`experimental-client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | `experimental` | — |
|
||||
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | — |
|
||||
| [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | — |
|
||||
| [`experimental-client-ui-creation-recommendations`](../packages/experimental/client-ui-creation-recommendations) | `experimental` | — |
|
||||
| [`experimental-client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | `experimental` | — |
|
||||
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | — |
|
||||
| [`experimental-cot-translation-bundle`](../packages/experimental/cot-translation-bundle) | `experimental` | — |
|
||||
@@ -1753,12 +1753,12 @@ flowchart TD
|
||||
| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-action-cards`](../packages/experimental/action-cards) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`action-cards`](../packages/experimental/action-cards) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`creation-recommendations`](../packages/experimental/creation-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-creation-recommendations`](../packages/experimental/creation-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
|
||||
| [`experimental-cua-macos`](../packages/experimental/cua-macos) | `experimental` | [`agent`](../packages/core/agent), [`computer-use`](../packages/computer-use/computer-use), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | `experimental` | [`experimental-worktree`](../packages/experimental/worktree), [`tools`](../packages/core/tools) |
|
||||
| [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
|
||||
@@ -174,8 +174,8 @@
|
||||
en: e5b55405bd2b69f5
|
||||
zh: e5b55405bd2b69f5
|
||||
/session-persistence-event-catalog/events/schedule/schedule-change-log-only:
|
||||
en: 162584304e006c72
|
||||
zh: d8a6bacf60777aa9
|
||||
en: e73c202fb99159b6
|
||||
zh: b1f43bbd116f83da
|
||||
/session-persistence-event-catalog/events/session:
|
||||
en: a1ac8f2660a4f61e
|
||||
zh: a1ac8f2660a4f61e
|
||||
|
||||
@@ -813,7 +813,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s
|
||||
|
||||
Types: [ScheduleChange](subsystems/schedule.md)
|
||||
|
||||
Source: [`packages/schedule/schedule/src/types.ts:365`](../packages/schedule/schedule/src/types.ts)
|
||||
Source: [`packages/schedule/schedule/src/types.ts:371`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
### `session/*`
|
||||
|
||||
|
||||
@@ -815,7 +815,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
|
||||
类型:[ScheduleChange](subsystems/schedule.zh.md)
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:365`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:371`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
### `session/*`
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
en: 93798ad69f74ddd5
|
||||
zh: 634126ad8accc356
|
||||
/host-wide-schedule/web-catalog:
|
||||
en: b5e5ac8125e65f33
|
||||
zh: 9ee064d833a4f838
|
||||
en: 3377a013bf8cbfb7
|
||||
zh: c5666dc3f28b087f
|
||||
/host-wide-schedule/timing-edits:
|
||||
en: ce5d2095a3ad2e82
|
||||
zh: 167b3715f8eaffc1
|
||||
|
||||
@@ -309,6 +309,14 @@ type ScheduleDeleteResult =
|
||||
| { readonly id: ScheduleId; readonly deleted: false; readonly code: 'schedule_not_found' }
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Successful Host-wide removal of every ended task, whether or not any remained. */
|
||||
interface ScheduleDeleteInactiveResult {
|
||||
/** Ended task rows this call removed; zero when the Host held none. */
|
||||
readonly deleted: number
|
||||
}
|
||||
```
|
||||
|
||||
## Web catalog
|
||||
|
||||
The Remote `schedule.catalog()` method returns active and inactive Host tasks with their original Session bindings, ordered by `scheduledAt` ascending and then lexicographically by `id`. It reads only the Schedule domain, without activating Sessions or reading their history. Browser consumers receive this browser-safe type:
|
||||
@@ -337,7 +345,7 @@ type ScheduleCatalogEntry = ScheduleRecord & {
|
||||
}
|
||||
```
|
||||
|
||||
The Remote `schedule.list({ sessionId })`, model `schedule_list`, and Session-header catalog return only active tasks. A model view's derived timing `state` remains distinct from stored lifecycle `status`. Deletion uses `schedule.delete({ sessionId, id })` with the entry's original binding; a mismatched binding returns not found. Model tools supply the current Agent's Session, whereas the global user interface supplies the selected task's binding. The binding check alone does not establish caller authorization. The payload-free `schedule/changed` event invalidates client lists; reconnecting clients fetch current state again.
|
||||
The Remote `schedule.list({ sessionId })`, model `schedule_list`, and Session-header catalog return only active tasks. A model view's derived timing `state` remains distinct from stored lifecycle `status`. Deletion uses `schedule.delete({ sessionId, id })` with the entry's original binding; a mismatched binding returns not found. Model tools supply the current Agent's Session, whereas the global user interface supplies the selected task's binding. The binding check alone does not establish caller authorization. `schedule.deleteInactive()` removes every ended (inactive) task across Sessions, with the saved delivery records each row carries; on success it answers `{ deleted }` with the count it removed; a normal failed batch rejects with nothing removed; an `unknown-state` failure may have removed rows and quarantines the Host schedule domain, so later schedule operations reject until it is reopened. Any rejection announces `schedule/changed`, and clients then re-read. The Automation tasks page's bulk cleanup uses it. An ended task never arms a timer, so the removal recomputes no target. The payload-free `schedule/changed` event invalidates client lists; reconnecting clients fetch current state again.
|
||||
|
||||
The shipped Web composition mounts `ui-schedule` with the Host capability. The [client package](../../packages/client/ui-schedule/README.md) owns the catalog, empty state, and deletion controls. The page separately filters all, active, and inactive tasks, retains inactive details and an original-Session control in the detail tab strip, and requires explicit confirmed deletion. The Rules and Records tabs separate task settings from lazily paged saved receipts. Neither a receipt nor inactive status confirms model execution.
|
||||
|
||||
@@ -508,6 +516,22 @@ async create(sessionId: SessionId, request: ScheduleCreateRequest, signal?: Abor
|
||||
*/
|
||||
@Remote('delete') async delete(request: ScheduleDeleteRequest, signal?: AbortSignal): Promise<ScheduleDeleteResult>
|
||||
|
||||
/**
|
||||
* Delete every ended Host task, leaving active tasks armed.
|
||||
*
|
||||
* Ended tasks never schedule another delivery, so the removal changes no pending
|
||||
* target and no timer is recomputed. Rows are enumerated and removed in one FIFO
|
||||
* slot, so a delivery ordered behind this call ends its task after the
|
||||
* enumeration rather than during it. The batch is one domain write: a normal
|
||||
* failure rejects with every listed row still stored, while a backend
|
||||
* `unknown-state` failure may have removed rows and quarantines the Host
|
||||
* domain, so later schedule operations reject until the domain is reopened.
|
||||
* Every rejection announces `schedule/changed` because it does not establish
|
||||
* which rows landed, and clients then re-read the catalog.
|
||||
* @returns Count of removed rows; zero when the Host held no ended task.
|
||||
*/
|
||||
@Remote('deleteInactive') async deleteInactive(): Promise<ScheduleDeleteInactiveResult>
|
||||
|
||||
/**
|
||||
* Update the name, instruction, and timing of an active task within the original Session
|
||||
* binding without activating the Session or changing saved deliveries.
|
||||
|
||||
@@ -309,6 +309,14 @@ type ScheduleDeleteResult =
|
||||
| { readonly id: ScheduleId; readonly deleted: false; readonly code: 'schedule_not_found' }
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Successful Host-wide removal of every ended task, whether or not any remained. */
|
||||
interface ScheduleDeleteInactiveResult {
|
||||
/** Ended task rows this call removed; zero when the Host held none. */
|
||||
readonly deleted: number
|
||||
}
|
||||
```
|
||||
|
||||
## Web 目录
|
||||
|
||||
Remote 方法 `schedule.catalog()` 返回活动和未运行的宿主任务及其原始 Session 绑定,先按 `scheduledAt` 升序排列,再按 `id` 字典序排列。它只读取 Schedule domain,不激活 Session 或读取其历史。浏览器消费方接收以下可安全用于浏览器的类型:
|
||||
@@ -337,7 +345,7 @@ type ScheduleCatalogEntry = ScheduleRecord & {
|
||||
}
|
||||
```
|
||||
|
||||
Remote 方法 `schedule.list({ sessionId })`、模型 `schedule_list` 和 Session 页头目录仅返回活动任务。模型视图派生的时间 `state` 与存储的生命周期 `status` 仍是不同概念。删除通过 `schedule.delete({ sessionId, id })` 使用条目的原始绑定;绑定不匹配时返回未找到。模型工具传入当前 Agent 的 Session,全局用户界面则传入所选任务的绑定。仅校验绑定并不构成调用者鉴权。无 payload 的 `schedule/changed` 事件通知客户端刷新列表;重新连接后,客户端再次获取当前状态。
|
||||
Remote 方法 `schedule.list({ sessionId })`、模型 `schedule_list` 和 Session 页头目录仅返回活动任务。模型视图派生的时间 `state` 与存储的生命周期 `status` 仍是不同概念。删除通过 `schedule.delete({ sessionId, id })` 使用条目的原始绑定;绑定不匹配时返回未找到。模型工具传入当前 Agent 的 Session,全局用户界面则传入所选任务的绑定。仅校验绑定并不构成调用者鉴权。`schedule.deleteInactive()` 删除跨会话的全部未运行(已结束)任务,连同每行携带的已保存的发送记录;成功时以它移除的计数答复 `{ deleted }`;普通失败时不删除任何行;后端报告 `unknown-state` 时可能已移除部分行,并会隔离 Host 的 schedule 领域,之后该领域的操作都会拒绝,直到重新打开。任何拒绝都会先发出 `schedule/changed`,客户端重新读取。自动化任务页面的批量清理使用该方法。未运行任务从不会安排定时器,因此该删除不重算任何目标。无 payload 的 `schedule/changed` 事件通知客户端刷新列表;重新连接后,客户端再次获取当前状态。
|
||||
|
||||
随发行版交付的 Web 组合把 `ui-schedule` 与宿主能力一起挂载。[客户端包](../../packages/client/ui-schedule/README.zh.md) 负责目录、空状态与删除控件。页面单独筛选全部、活动和未运行任务,保留未运行任务详情及详情页签条内的原 Session 入口,并要求显式确认删除。“规则”和“任务运行记录”页签将任务设置与按需分页加载的已保存回执分开。回执与未运行状态均不确认模型执行。
|
||||
|
||||
@@ -508,6 +516,22 @@ async create(sessionId: SessionId, request: ScheduleCreateRequest, signal?: Abor
|
||||
*/
|
||||
@Remote('delete') async delete(request: ScheduleDeleteRequest, signal?: AbortSignal): Promise<ScheduleDeleteResult>
|
||||
|
||||
/**
|
||||
* Delete every ended Host task, leaving active tasks armed.
|
||||
*
|
||||
* Ended tasks never schedule another delivery, so the removal changes no pending
|
||||
* target and no timer is recomputed. Rows are enumerated and removed in one FIFO
|
||||
* slot, so a delivery ordered behind this call ends its task after the
|
||||
* enumeration rather than during it. The batch is one domain write: a normal
|
||||
* failure rejects with every listed row still stored, while a backend
|
||||
* `unknown-state` failure may have removed rows and quarantines the Host
|
||||
* domain, so later schedule operations reject until the domain is reopened.
|
||||
* Every rejection announces `schedule/changed` because it does not establish
|
||||
* which rows landed, and clients then re-read the catalog.
|
||||
* @returns Count of removed rows; zero when the Host held no ended task.
|
||||
*/
|
||||
@Remote('deleteInactive') async deleteInactive(): Promise<ScheduleDeleteInactiveResult>
|
||||
|
||||
/**
|
||||
* Update the name, instruction, and timing of an active task within the original Session
|
||||
* binding without activating the Session or changing saved deliveries.
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
en: b11e06e7c0e0fd13
|
||||
zh: 4585f26ff416cb41
|
||||
/storage/the-backend-contract:
|
||||
en: 2316787902c6a94e
|
||||
zh: e3bda06e1c33acac
|
||||
en: 225e25b9b4b08865
|
||||
zh: 53bd3554da610607
|
||||
/storage/declaring-a-domain:
|
||||
en: 775cce941500f01f
|
||||
zh: 78acbc67f48574cb
|
||||
/storage/the-open-domain:
|
||||
en: 2468b5df66c49b51
|
||||
zh: 1587f460e9769db2
|
||||
en: b69b25bc06f1c052
|
||||
zh: 8aca58718da520c7
|
||||
/storage/the-domain-facility-ctx-storagedomain:
|
||||
en: deed74fcd4506ef4
|
||||
zh: 8e3e6e5d2dc2e989
|
||||
|
||||
@@ -44,7 +44,7 @@ interface StorageBackend {
|
||||
}
|
||||
```
|
||||
|
||||
A backend owns one medium (a file-tree root, a database file) and exposes optional operation groups; `kv` is the only shipped group. `KvFacet.open(descriptor)` opens one named unit — `KvUnitDescriptor` carries the name, current format version, optional compatible record versions, table names, and whether a global singleton slot exists — and returns a `KvUnit` with `loadAll`, `putRecord`, `deleteRecord`, `setGlobal`, and `close`. Unit and table names must match `UNIT_NAME_RE` (safe as a file name and as a SQL identifier segment); record keys are arbitrary strings that never reach file paths. A unit does not serialize concurrent writes — ordering belongs to the caller — but each single call is atomic on the medium and durable once resolved. A `single` medium stamped with a different version rejects `version-mismatch`; a `per-record` document stamped outside the accepted set reads as absent. A medium that cannot be parsed as the unit rejects `malformed-medium`. [`backend.ts`](../../packages/storage/storage/src/backend.ts) is the normative clause-by-clause contract, and the shared conformance suite in [`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) checks every clause against each backend. The [json backend](../../packages/storage/storage-json/README.md) republishes one whole human-readable file per unit atomically; the [sqlite backend](../../packages/storage/storage-sqlite/README.md) stores one document per row in one database for frequently updated data.
|
||||
A backend owns one medium (a file-tree root, a database file) and exposes optional operation groups; `kv` is the only shipped group. `KvFacet.open(descriptor)` opens one named unit — `KvUnitDescriptor` carries the name, current format version, optional compatible record versions, table names, and whether a global singleton slot exists — and returns a `KvUnit` with `loadAll`, `putRecord`, `deleteRecord`, `deleteRecords`, `setGlobal`, and `close`. Unit and table names must match `UNIT_NAME_RE` (safe as a file name and as a SQL identifier segment); record keys are opaque strings in the `single` layout and match `[a-zA-Z0-9_-]+` path segments in the `per-record` layout. A unit does not serialize concurrent writes — ordering belongs to the caller — but each single call is atomic on the medium; durability holds for single-record writes and whole-file publishes, while a `per-record` batch deletion relies on the filesystem persisting directory-entry updates. `deleteRecords` applies a whole batch in one call: it resolves once every listed record is absent from the readable state and the backend accepted the batch, and a rejection normally leaves the batch unapplied; a backend that cannot establish or restore the batch's state rejects with `unknown-state`, after which every later operation on that unit rejects with the same error until `close()` releases it (calls after close reject `closed`). A `single`-layout failure after the atomic replacement (a directory-fsync error) can leave the new file on the medium while the caller sees a rejection; the unit keeps its memory consistent and the next publish rewrites the file. A `single` medium stamped with a different version rejects `version-mismatch`; a `per-record` document stamped outside the accepted set reads as absent. A medium that cannot be parsed as the unit rejects `malformed-medium`. [`backend.ts`](../../packages/storage/storage/src/backend.ts) is the normative clause-by-clause contract, and the shared conformance suite in [`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) checks the shared clauses against each backend. The [json backend](../../packages/storage/storage-json/README.md) republishes one whole human-readable file per unit atomically; the [sqlite backend](../../packages/storage/storage-sqlite/README.md) stores one document per row in one database for frequently updated data.
|
||||
|
||||
## Declaring a domain
|
||||
|
||||
@@ -123,7 +123,7 @@ interface Domain<S extends DomainSpec> {
|
||||
}
|
||||
```
|
||||
|
||||
Reads are synchronous from authoritative in-memory state: `KvTable` exposes `get`/`entries`/`keys`/`size` (snapshot iterators that stay stable while queued writes land), and the global handle's `get()` serves the spec's `initial` until the first `set` materializes the slot on the medium. Every write — `put`, `delete`, `update`, `global.set` — queues on one per-domain chain and reaches backend durability first, then mutates memory, then emits `domain/changed`; a rejected backend write leaves memory untouched, so reads never diverge from the medium. `update(key, fn)` is an atomic read-modify-write at its chain slot (a missing key rejects `missing-key`); `delete` of an absent key resolves `false` with no write and no event. Returned records are the stored objects themselves, not copies — replace via `put`/`update`, never mutate in place.
|
||||
Reads are synchronous from authoritative in-memory state: `KvTable` exposes `get`/`entries`/`keys`/`size` (snapshot iterators that stay stable while queued writes land), and the global handle's `get()` serves the spec's `initial` until the first `set` materializes the slot on the medium. Every write — `put`, `delete`, `deleteMany`, `update`, `global.set` — queues on one per-domain chain and reaches backend durability first, then mutates memory, then emits `domain/changed`; a rejected backend write leaves memory untouched, so reads never diverge from the medium; a backend that reports `unknown-state` instead quarantines the domain, and its reads and later writes reject until `close()` releases it. `update(key, fn)` is an atomic read-modify-write at its chain slot (a missing key rejects `missing-key`); `delete` of an absent key resolves `false` with no write and no event. `deleteMany(keys)` removes the present keys as one backend batch in one chain slot, resolves the removed count, and emits one `domain/changed` per removed key; a rejected backend write leaves every record in place and emits nothing; an `unknown-state` rejection quarantines the domain. Returned records are the stored objects themselves, not copies — replace via `put`/`update`, never mutate in place.
|
||||
|
||||
## The domain facility: `ctx.storageDomain`
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ interface StorageBackend {
|
||||
}
|
||||
```
|
||||
|
||||
一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并提供可选的操作组;`kv` 是唯一已交付的操作组。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、当前格式版本、可选的兼容记录版本、表名清单,以及是否存在全局单例 slot——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键是任意字符串,绝不进入文件路径。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的,且 resolve 后即已持久。`single` 介质上记录的版本不同时拒绝 `version-mismatch`;`per-record` 文档的版本在接受集合之外时读作不存在。无法按该 unit 解析的介质拒绝 `malformed-medium`。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性约定,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件会针对每个后端检查每项条款。[json 后端](../../packages/storage/storage-json/README.zh.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.zh.md)在单个数据库中每行存储一份文档,用于频繁更新的数据。
|
||||
一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并提供可选的操作组;`kv` 是唯一已交付的操作组。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、当前格式版本、可选的兼容记录版本、表名清单,以及是否存在全局单例 slot——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`deleteRecords`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键在 `single` 布局中是不透明字符串,在 `per-record` 布局中则是匹配 `[a-zA-Z0-9_-]+` 的路径片段。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的;单记录写入与整文件发布在 resolve 后即已持久,`per-record` 批量删除则依赖文件系统对目录项更新的持久化。`deleteRecords` 以一次调用应用整批删除:所有列出的记录都从可读状态消失且后端接受该批后才 resolve,拒绝时通常整批未应用;无法建立或恢复批次状态的批量删除会以 `unknown-state` 拒绝,之后该 unit 的所有操作都以同一错误拒绝,直到 `close()` 释放它(关闭后的调用以 `closed` 拒绝)。`single` 布局在原子替换之后失败(目录 fsync 错误)时,新文件可能已落在介质上而调用方看到拒绝;unit 的内存保持一致,下一次发布会重写该文件。`single` 介质上记录的版本不同时拒绝 `version-mismatch`;`per-record` 文档的版本在接受集合之外时读作不存在。无法按该 unit 解析的介质拒绝 `malformed-medium`。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性约定,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件会针对每个后端检查共享条款。[json 后端](../../packages/storage/storage-json/README.zh.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.zh.md)在单个数据库中每行存储一份文档,用于频繁更新的数据。
|
||||
|
||||
## 声明领域
|
||||
|
||||
@@ -123,7 +123,7 @@ interface Domain<S extends DomainSpec> {
|
||||
}
|
||||
```
|
||||
|
||||
读取是同步的,来自权威的内存态:`KvTable` 暴露 `get`/`entries`/`keys`/`size`(快照迭代器,在排队写入落地期间保持稳定),global 句柄的 `get()` 在第一次 `set` 将 slot 物化到介质之前一直返回 spec 的 `initial`。每次写入——`put`、`delete`、`update`、`global.set`——都在同一条逐领域写链上排队,先在后端完成持久化,再更新内存,最后发出 `domain/changed`;后端写入被拒时内存原样不动,因此读取绝不会偏离介质。`update(key, fn)` 在其写链 slot 上是一次原子的读-改-写(键缺失时拒绝 `missing-key`);`delete` 一个不存在的键 resolve 为 `false`,不产生写入也不产生事件。返回的记录就是存储的对象本身,不是副本——请经 `put`/`update` 整体替换,绝不要就地修改。
|
||||
读取是同步的,来自权威的内存态:`KvTable` 暴露 `get`/`entries`/`keys`/`size`(快照迭代器,在排队写入落地期间保持稳定),global 句柄的 `get()` 在第一次 `set` 将 slot 物化到介质之前一直返回 spec 的 `initial`。每次写入——`put`、`delete`、`deleteMany`、`update`、`global.set`——都在同一条逐领域写链上排队,先在后端完成持久化,再更新内存,最后发出 `domain/changed`;后端写入被拒时内存原样不动,因此读取绝不会偏离介质;若后端报告 `unknown-state`,领域会被隔离,读取与后续写入都会拒绝,直到 `close()` 释放它。`update(key, fn)` 在其写链 slot 上是一次原子的读-改-写(键缺失时拒绝 `missing-key`);`delete` 一个不存在的键 resolve 为 `false`,不产生写入也不产生事件。`deleteMany(keys)` 在一个写链 slot 内把存在的键作为一次后端批量删除移除,resolve 为移除数量,并为每个被移除的键发一次 `domain/changed`;后端写入被拒时所有记录保持原样,也不发事件;`unknown-state` 拒绝会隔离整个领域。返回的记录就是存储的对象本身,不是副本——请经 `put`/`update` 整体替换,绝不要就地修改。
|
||||
|
||||
## 领域 facility:`ctx.storageDomain`
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 6cc2cd9ced70bb09
|
||||
zh: 1c0e6cb4ba2ed637
|
||||
/tool-schema-catalog/tool-package-map:
|
||||
en: 7885592c710ce003
|
||||
zh: c89bd1909196347f
|
||||
en: c089eeb9ebe832b1
|
||||
zh: 97f6ac552abb55e3
|
||||
/tool-schema-catalog/deepseek-ai-dsh-plugin-manager:
|
||||
en: e0143916bb634762
|
||||
zh: e0143916bb634762
|
||||
@@ -54,12 +54,12 @@
|
||||
en: 4910ab327694ee97
|
||||
zh: 17e79e4366cb636d
|
||||
/tool-schema-catalog/deepseek-ai-dsh-experimental-browser-use-stagehand-native/stagehand-tabs:
|
||||
en: c65d58eac812b03c
|
||||
zh: d4259095ce06e457
|
||||
/tool-schema-catalog/deepseek-ai-dsh-experimental-action-cards:
|
||||
en: 94f08f350e306557
|
||||
zh: 94f08f350e306557
|
||||
/tool-schema-catalog/deepseek-ai-dsh-experimental-action-cards/propose-action:
|
||||
en: a0d5b9b891358120
|
||||
zh: 25793a62709770b5
|
||||
/tool-schema-catalog/deepseek-ai-dsh-action-cards:
|
||||
en: 60c9df6ffda98a1d
|
||||
zh: 60c9df6ffda98a1d
|
||||
/tool-schema-catalog/deepseek-ai-dsh-action-cards/propose-action:
|
||||
en: c289411d447b015a
|
||||
zh: 3a770388e90df4df
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-ask-user:
|
||||
|
||||
@@ -19,7 +19,7 @@ This table connects model-visible tool names to the plugin package and service s
|
||||
| `@deepseek-ai/dsh-plugin-market` | `plugin_market` | `ctx.tools`, `ctx.pluginMarket` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`, `stagehand_extract`, `stagehand_navigate`, `stagehand_observe`, `stagehand_screenshot`, `stagehand_tabs` | `ctx.browserUse`, `ctx.agents`, `ctx.tools`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-experimental-action-cards` | `propose_action` | `ctx.tools`, `ctx.agents`, `ctx.sessionProjections`, `ctx.remoteAccess` | `tool/call`, `tool/result`, `plugin:action-cards/decision`, `user/message` | - | Declared only to an Agent that loaded the skill of a registered card kind; the kind packages register their actions, and the Personalized pages and Plugin recommendations bundles supply the shipped kinds. |
|
||||
| `@deepseek-ai/dsh-action-cards` | `propose_action` | `ctx.tools`, `ctx.agents`, `ctx.sessionProjections`, `ctx.remoteAccess` | `tool/call`, `tool/result`, `plugin:action-cards/decision`, `user/message` | - | Declared only to an Agent that loaded the skill of a registered card kind; the kind packages register their actions, and the Personalized pages and Plugin recommendations bundles supply the shipped kinds. |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after an answer or timeout`, `late user/message` | - | ask_user_question keeps the original blocking behavior by default; set `mode: timed` to opt into a foreground timeout and pending result while the question remains answerable. In timed mode, `timeout: -1` keeps that call blocking indefinitely. |
|
||||
| `@deepseek-ai/dsh-tool-executor-ptc` | `run_code` | `ctx.workingDirectory (Agent executions)`, `ctx.tools`, `ctx.ptcRuntime`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | The PTC executor exposes only run_code to the model and declares other visible capabilities in a generated SDK in the runtime’s language. Program bindings enter the shared guarded tool pipeline in submission order; concurrency-safe bodies overlap up to maxParallelSubCalls, and nested results remain linked to the outer program. |
|
||||
| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agents`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. |
|
||||
@@ -498,9 +498,9 @@ List, create, select, or close a Stagehand browser tab.
|
||||
|
||||
Source: [`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-experimental-action-cards"></a>
|
||||
<a id="deepseek-aidsh-action-cards"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-experimental-action-cards`
|
||||
## `@deepseek-ai/dsh-action-cards`
|
||||
|
||||
### `propose_action`
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
| `@deepseek-ai/dsh-plugin-market` | `plugin_market` | `ctx.tools`, `ctx.pluginMarket` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`、`stagehand_extract`、`stagehand_navigate`、`stagehand_observe`、`stagehand_screenshot`、`stagehand_tabs` | `ctx.browserUse`、`ctx.agents`、`ctx.tools`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-experimental-action-cards` | `propose_action` | `ctx.tools`、`ctx.agents`、`ctx.sessionProjections`、`ctx.remoteAccess` | `tool/call`、`tool/result`、`plugin:action-cards/decision`、`user/message` | - | 只向加载过已注册卡片种类 Skill 的 Agent 声明;各种类的包注册自己的动作,个性化页面和插件推荐两个 Bundle 提供随产品发布的种类。 |
|
||||
| `@deepseek-ai/dsh-action-cards` | `propose_action` | `ctx.tools`、`ctx.agents`、`ctx.sessionProjections`、`ctx.remoteAccess` | `tool/call`、`tool/result`、`plugin:action-cards/decision`、`user/message` | - | 只向加载过已注册卡片种类 Skill 的 Agent 声明;各种类的包注册自己的动作,个性化页面和插件推荐两个 Bundle 提供随产品发布的种类。 |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after an answer or timeout`、`late user/message` | - | ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才启用前台超时与 pending 结果,同时问题仍可回答;timed 模式内 `timeout: -1` 让本次调用无限期阻塞。 |
|
||||
| `@deepseek-ai/dsh-tool-executor-ptc` | `run_code` | `ctx.workingDirectory (Agent executions)`、`ctx.tools`、`ctx.ptcRuntime`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | PTC 执行器只向模型公开 run_code,并在运行时语言生成的 SDK 中声明其他可见能力。程序绑定按提交顺序进入共享的受守卫工具流水线;并行安全的函数体最多重叠 maxParallelSubCalls 个,嵌套结果保持与外层程序关联。 |
|
||||
| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agents`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 |
|
||||
@@ -502,9 +502,9 @@
|
||||
|
||||
来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-experimental-action-cards"></a>
|
||||
<a id="deepseek-aidsh-action-cards"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-experimental-action-cards`
|
||||
## `@deepseek-ai/dsh-action-cards`
|
||||
|
||||
### `propose_action`
|
||||
|
||||
|
||||
+1
-1
@@ -301,6 +301,6 @@
|
||||
"@deepseek-ai/dsh-experimental-subagent-oar": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-tool-subagent-oar": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-cua-macos": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-action-cards": "workspace:*"
|
||||
"@deepseek-ai/dsh-action-cards": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export const DEFAULT_PROFILE_BUNDLES: readonly string[] = ['@deepseek-ai/dsh-bas
|
||||
* [admission](../../../../.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md)).
|
||||
*/
|
||||
export const OPTIONAL_BUNDLES: readonly string[] = [
|
||||
'@deepseek-ai/dsh-experimental-creation-recommendations',
|
||||
'@deepseek-ai/dsh-creation-recommendations',
|
||||
'@deepseek-ai/dsh-experimental-plugin-recommendations',
|
||||
'@deepseek-ai/dsh-cua-bundle',
|
||||
'@deepseek-ai/dsh-experimental-session-search',
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 41dcd70e14ebe9cc
|
||||
zh: ededead572b59037
|
||||
/deepseek-ai-dsh-client-ui-chat/turn-token-usage:
|
||||
en: 059eef868161efe4
|
||||
zh: 52ecc8e682d4c630
|
||||
en: cfdfe3d99d3f4e09
|
||||
zh: 9330bad3d3f7a689
|
||||
/deepseek-ai-dsh-client-ui-chat/completed-turn-footer:
|
||||
en: 382821ca3ffa60dd
|
||||
zh: 9840ffa07c13082a
|
||||
|
||||
@@ -70,7 +70,7 @@ A completed Turn shows an expandable usage row only when the loaded window inclu
|
||||
|
||||
Settings → General → Performance & usage appears after Send behavior while busy and before Session Log upload, and stores `ui-chat.performanceUsage` as `detailed` (default) or `compact`. Compact shows only available output speed and cache-hit percentage beneath the composer, without interactive statistic dialogs or per-Turn usage. Detailed exposes session statistics and per-Turn token usage. Neither mode shows elapsed time in the completed-turn footer. The preference changes presentation only; accounting and Session events remain intact.
|
||||
|
||||
The composer statistics are two `conversation.composer.dock` list entries: `activity` (order 0, turn and step counts and output speed, with LLM time, tool time, and TTFT) and `usage` (order 1, token total and cache hit). The controls follow the shared [statistics-control radius](../../../docs/ui-radius.md#radius-scale). Another plugin replaces one control by registering the same id and the same `order`; the dock orders rows by `order`, while a lower `priority` picks the replacement: a dynamic plugin receives one automatically, and a statically composed plugin passes it explicitly. A new id adds a control. Each control carries its id in `data-composer-stat`. An open statistics dialog closes on Escape or on a pointerdown or click outside it, so activating another control by pointer or keyboard swaps dialogs instead of stacking them.
|
||||
The composer statistics are two `conversation.composer.dock` list entries: `activity` (order 0, turn and step counts and output speed, with LLM time, tool time, and TTFT) and `usage` (order 1, token total and cache hit). The controls follow the shared [statistics-control radius](../../../docs/ui-radius.md#radius-scale). Another plugin replaces one control by registering the same id and the same `order`; the dock orders rows by `order`, while a lower `priority` picks the replacement: a dynamic plugin receives one automatically, and a statically composed plugin passes it explicitly. A new id adds a control. Each control carries its id in `data-composer-stat`. An open statistics dialog closes on Escape or on a pointerdown or click outside it, so activating another control by pointer or keyboard swaps dialogs instead of stacking them. When an ellipsis cuts off a reading, a tooltip shows the full reading. A dialog control shows it on hover or keyboard focus, but not while its dialog is open; a reading without a dialog shows it on hover, click, or tap. The session statistics dialog heads its rows with the turn and step counts.
|
||||
|
||||
Explicit selections update every consumer immediately; accepted Host settings reconcile the live value, so a refused or pending write keeps the selected mode until the Host answers.
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node,
|
||||
|
||||
设置 → 通用设置 → 性能与用量位于繁忙时的发送行为之后、Session Log 上传之前,将 `ui-chat.performanceUsage` 保存为 `detailed`(默认)或 `compact`。简洁模式仅在输入框下方显示可用的输出速度和缓存命中率,不显示统计交互卡片或每轮用量。详细模式提供会话统计和每轮 token 用量。两种模式的已完成轮次页脚均不显示耗时。该偏好仅影响展示,记账和 Session 事件保持完整。
|
||||
|
||||
输入框统计信息由两个 `conversation.composer.dock` 列表条目组成:`activity`(order 0,轮次数、步骤数与输出速度,以及 LLM 耗时、工具耗时与 TTFT)和 `usage`(order 1,token 总量与缓存命中)。这些控件遵循公共的[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。其他插件以相同的 id 和相同的 `order` 注册即可替换某个控件;dock 按 `order` 排列各行,更低的 `priority` 决定由替换者渲染:动态插件会自动获得,静态组合的插件需显式传入。以新 id 注册则新增控件。每个控件在 `data-composer-stat` 中携带其 id。按下 Escape,或在已打开的对话框之外发生 pointerdown 或点击时,该对话框会关闭,因此用指针或键盘激活另一个控件会切换对话框,而不会叠加两个。
|
||||
输入框统计信息由两个 `conversation.composer.dock` 列表条目组成:`activity`(order 0,轮次数、步骤数与输出速度,以及 LLM 耗时、工具耗时与 TTFT)和 `usage`(order 1,token 总量与缓存命中)。这些控件遵循公共的[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。其他插件以相同的 id 和相同的 `order` 注册即可替换某个控件;dock 按 `order` 排列各行,更低的 `priority` 决定由替换者渲染:动态插件会自动获得,静态组合的插件需显式传入。以新 id 注册则新增控件。每个控件在 `data-composer-stat` 中携带其 id。按下 Escape,或在已打开的对话框之外发生 pointerdown 或点击时,该对话框会关闭,因此用指针或键盘激活另一个控件会切换对话框,而不会叠加两个。读数被省略号截断时,提示气泡会显示完整读数。带对话框的控件在悬停或键盘聚焦时显示,对话框打开期间不显示;没有对话框的读数在悬停、点击或轻触时显示。会话统计对话框的标题行显示轮次数与步骤数。
|
||||
|
||||
明确选择会立即更新所有使用方;Host 接受的设置会校正实时值,因此写入被拒绝或尚未应答时保持所选模式,直到 Host 应答。
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { memo, useMemo, type ReactNode } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { IconDatabaseOutlineRegular, IconGaugeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { IconDatabaseOutlineRegular, IconGaugeOutlineRegular, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { InjectFace, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
// Type-only: merges the sessionStats key into SessionProjectionMap for useProjection.
|
||||
@@ -136,21 +136,27 @@ interface PillContent {
|
||||
stat: StatId
|
||||
icon: ReactNode
|
||||
label: ReactNode
|
||||
/** The full reading as plain text: the accessible name and the tooltip shown while the label is cut off. */
|
||||
text: string
|
||||
}
|
||||
|
||||
function exactCount(value: number, t: Translate): string {
|
||||
return t('message.turnUsage.count', { count: formatExactTokens(value, t) })
|
||||
}
|
||||
|
||||
function joined(first: string, second: string | null): ReactNode {
|
||||
if (second === null) return first
|
||||
return (
|
||||
<>
|
||||
{first}
|
||||
<span className={css.sep} aria-hidden>·</span>
|
||||
{second}
|
||||
</>
|
||||
)
|
||||
/** One or two reading segments, visually joined by a separator glyph. */
|
||||
function reading(first: string, second: string | null): Pick<PillContent, 'label' | 'text'> {
|
||||
if (second === null) return { label: first, text: first }
|
||||
return {
|
||||
label: (
|
||||
<>
|
||||
{first}
|
||||
<span className={css.sep} aria-hidden>·</span>
|
||||
{second}
|
||||
</>
|
||||
),
|
||||
text: `${first} · ${second}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Every figure rides the durable sessionStats projection, so paging and
|
||||
@@ -169,14 +175,20 @@ function decodeSpeed(stats: WindowStats, t: Translate): string {
|
||||
})
|
||||
}
|
||||
|
||||
/** The full reading above a pill, only while an ellipsis cuts its label; the delay matches the dock's context meter. */
|
||||
const FULL_READING_TOOLTIP = { side: 'top', portal: true, delayMs: 200, onlyWhenClipped: true } as const
|
||||
|
||||
/** A static reading: used when the pill has no dialog rows or the mode is Compact. */
|
||||
function PlainPill({ stat, icon, label }: PillContent) {
|
||||
function PlainPill({ stat, icon, label, text }: PillContent) {
|
||||
return (
|
||||
<span className={css.anchor} data-composer-stat={stat}>
|
||||
<span className={css.pill}>
|
||||
{icon}
|
||||
<span className={css.label}>{label}</span>
|
||||
</span>
|
||||
{/* No dialog carries a static reading, so a tap also pins the full text. */}
|
||||
<Tooltip label={text} {...FULL_READING_TOOLTIP} openOnClick>
|
||||
<span className={css.pill}>
|
||||
{icon}
|
||||
<span className={css.label}>{label}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -186,8 +198,7 @@ function PlainPill({ stat, icon, label }: PillContent) {
|
||||
* state; useStatDialog closes it on Escape or an outside pointerdown or click,
|
||||
* so at most one dialog is open across the dock.
|
||||
*/
|
||||
function DialogPill({ stat, icon, label, ariaLabel, title, titleValue, children }: PillContent & {
|
||||
ariaLabel: string
|
||||
function DialogPill({ stat, icon, label, text, title, titleValue, children }: PillContent & {
|
||||
title: string
|
||||
titleValue?: string
|
||||
children: ReactNode
|
||||
@@ -195,17 +206,20 @@ function DialogPill({ stat, icon, label, ariaLabel, title, titleValue, children
|
||||
const { open, setOpen, rootRef, panelRef, pos } = useStatDialog()
|
||||
return (
|
||||
<span ref={rootRef} className={css.anchor} data-composer-stat={stat}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.pill}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
aria-label={ariaLabel}
|
||||
onClick={() => { setOpen(!open) }}
|
||||
>
|
||||
{icon}
|
||||
<span className={css.label}>{label}</span>
|
||||
</button>
|
||||
{/* The open dialog already shows every figure; a tooltip would cover it. */}
|
||||
<Tooltip label={text} {...FULL_READING_TOOLTIP} disabled={open}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.pill}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
aria-label={text}
|
||||
onClick={() => { setOpen(!open) }}
|
||||
>
|
||||
{icon}
|
||||
<span className={css.label}>{label}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{open && createPortal(
|
||||
<div
|
||||
ref={panelRef}
|
||||
@@ -239,15 +253,16 @@ export const ActivityPill = memo(function ActivityPill({ useChat, useProjection,
|
||||
const stats = useSessionStats(useChat, useProjection)
|
||||
const speed = stats.decodeMs > 0 ? decodeSpeed(stats, t) : null
|
||||
const icon = <IconGaugeOutlineRegular />
|
||||
if (mode === 'compact') return speed === null ? null : <PlainPill stat="activity" icon={icon} label={speed} />
|
||||
if (mode === 'compact') return speed === null ? null : <PlainPill stat="activity" icon={icon} {...reading(speed, null)} />
|
||||
if (stats.steps === 0) return null
|
||||
const counts = t('stats.counts', { turns: stats.turns, steps: stats.steps })
|
||||
const content: PillContent = { stat: 'activity', icon, label: joined(counts, speed) }
|
||||
const content: PillContent = { stat: 'activity', icon, ...reading(counts, speed) }
|
||||
// A window without one timed figure has no dialog rows to show, so the pill
|
||||
// stays a plain reading instead of a button opening an empty dialog.
|
||||
if (stats.llmMs <= 0 && stats.toolMs <= 0 && stats.ttftSteps <= 0 && speed === null) return <PlainPill {...content} />
|
||||
return (
|
||||
<DialogPill {...content} ariaLabel={speed === null ? counts : `${counts} · ${speed}`} title={t('stats.dialog.title')}>
|
||||
// The counts head the dialog, so a cut-off reading stays complete once the dialog opens.
|
||||
<DialogPill {...content} title={t('stats.dialog.title')} titleValue={counts}>
|
||||
<dl className={dialogCss.details} data-session-stats-details>
|
||||
{stats.llmMs > 0 && (
|
||||
<>
|
||||
@@ -289,7 +304,7 @@ export const UsagePill = memo(function UsagePill({ useProjection, usePerformance
|
||||
const cacheHitText = cacheHit !== null ? t('stats.cacheHit', { percent: cacheHit }) : null
|
||||
const icon = <IconDatabaseOutlineRegular />
|
||||
if (mode === 'compact') {
|
||||
return cacheHitText === null ? null : <PlainPill stat="usage" icon={icon} label={cacheHitText} />
|
||||
return cacheHitText === null ? null : <PlainPill stat="usage" icon={icon} {...reading(cacheHitText, null)} />
|
||||
}
|
||||
// Same aggregate as the Turn pill's totalTokens: every prompt-side billing bucket plus output.
|
||||
const total = billedInputTokens(usage) + usage.outputTokens
|
||||
@@ -298,8 +313,7 @@ export const UsagePill = memo(function UsagePill({ useProjection, usePerformance
|
||||
<DialogPill
|
||||
stat="usage"
|
||||
icon={icon}
|
||||
label={joined(totalText, cacheHitText)}
|
||||
ariaLabel={cacheHitText === null ? totalText : `${totalText} · ${cacheHitText}`}
|
||||
{...reading(totalText, cacheHitText)}
|
||||
title={t('stats.dialog.usageTitle')}
|
||||
titleValue={exactCount(total, t)}
|
||||
>
|
||||
|
||||
@@ -268,7 +268,7 @@ describe('composer stats pills', () => {
|
||||
expect(dialog.getAttribute('aria-label')).toBe('Session statistics')
|
||||
// Portaled out of the composer dock.
|
||||
expect(dialog.parentElement).toBe(document.body)
|
||||
expect(dialog.firstChild?.textContent).toBe('Session statistics')
|
||||
expect(dialog.firstChild?.textContent).toBe('Session statistics1 turns 1 steps')
|
||||
const details = dialog.querySelector('[data-session-stats-details]') as HTMLElement
|
||||
expect(details).toBeTruthy()
|
||||
expect(details.textContent).toContain('LLM time3.8s')
|
||||
@@ -375,6 +375,53 @@ describe('composer stats pills', () => {
|
||||
expect(usageDialog.textContent).toContain('未缓存输入5 tok')
|
||||
})
|
||||
|
||||
describe('full reading of a cut-off pill', () => {
|
||||
const FULL = '105 tok · Cache hit 90%'
|
||||
|
||||
function renderCut(overflow: boolean, mode: 'detailed' | 'compact' = 'detailed') {
|
||||
vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockReturnValue(overflow ? 200 : 80)
|
||||
vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(80)
|
||||
vi.useFakeTimers()
|
||||
const { source } = makeSource({ nodes: [timedStep()] })
|
||||
return render(<StatsPills {...props(source)} usePerformanceUsage={selector => selector(mode)} />)
|
||||
}
|
||||
const tooltipText = (view: ReturnType<typeof render>) => view.queryByRole('tooltip')?.textContent ?? null
|
||||
afterEach(() => { vi.useRealTimers() })
|
||||
|
||||
it.each([false, true])('shows on hover after the dock delay only while the label is cut off: %s', (overflow) => {
|
||||
const view = renderCut(overflow)
|
||||
fireEvent.pointerEnter(view.getAllByRole('button')[1]!)
|
||||
expect(tooltipText(view)).toBeNull()
|
||||
act(() => { vi.advanceTimersByTime(200) })
|
||||
expect(tooltipText(view)).toBe(overflow ? FULL : null)
|
||||
})
|
||||
|
||||
it('shows on keyboard focus of a dialog pill', () => {
|
||||
const view = renderCut(true)
|
||||
fireEvent.keyDown(document.body, { key: 'Tab' })
|
||||
fireEvent.focus(view.getAllByRole('button')[1]!)
|
||||
expect(tooltipText(view)).toBe(FULL)
|
||||
})
|
||||
|
||||
it('yields to the open dialog, whose head carries the counts', () => {
|
||||
const view = renderCut(true)
|
||||
const [timePill] = view.getAllByRole('button') as [HTMLElement]
|
||||
fireEvent.click(timePill)
|
||||
fireEvent.pointerEnter(timePill)
|
||||
act(() => { vi.advanceTimersByTime(200) })
|
||||
expect(tooltipText(view)).toBeNull()
|
||||
expect(view.getByRole('dialog').firstChild?.textContent).toBe('Session statistics1 turns 1 steps')
|
||||
})
|
||||
|
||||
it('pins a cut-off static reading on tap', () => {
|
||||
const view = renderCut(true, 'compact')
|
||||
const plainPill = view.getByText('Cache hit 90%').parentElement!
|
||||
fireEvent.pointerDown(plainPill, { pointerType: 'touch' })
|
||||
fireEvent.click(plainPill)
|
||||
expect(tooltipText(view)).toBe('Cache hit 90%')
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the durable usage pill after the visible step window is empty', () => {
|
||||
const { source } = makeSource()
|
||||
const view = render(<StatsPills {...props(source, {
|
||||
|
||||
@@ -215,12 +215,20 @@
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
/* The 16px text line retains 9px below it. The active bar reaches 1px past
|
||||
the tab box to sit over the header rule. */
|
||||
/* The 16px text line retains 9px below it. The press target extends 10px
|
||||
above the text into the strip's top margin and 10px to each side, matching
|
||||
the inline padding of the Sidebar tab chips (ui-dockkit `.tab`); negative
|
||||
margins keep the layout box at its 25px row and text width. The active bar
|
||||
spans the text and reaches 1px past the tab box to sit over the header
|
||||
rule. */
|
||||
.tab {
|
||||
--dsh-tab-hit-inline: 10px;
|
||||
|
||||
position: relative;
|
||||
min-height: 25px;
|
||||
padding: 0 0 9px;
|
||||
box-sizing: border-box;
|
||||
min-height: 35px;
|
||||
margin: -10px calc(-1 * var(--dsh-tab-hit-inline)) 0;
|
||||
padding: 10px var(--dsh-tab-hit-inline) 9px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
@@ -233,9 +241,9 @@
|
||||
.tab::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
right: var(--dsh-tab-hit-inline);
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
left: var(--dsh-tab-hit-inline);
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
|
||||
@@ -8,8 +8,10 @@ describe('conversation header geometry', () => {
|
||||
it('keeps the single-line header at the shared 76px height', () => {
|
||||
expect(css.match(/\.header\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 76px')
|
||||
expect(css.match(/\.viewTabs\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 25px')
|
||||
expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 25px')
|
||||
expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('padding: 0 0 9px')
|
||||
// The tab's 35px press target sits 10px above its 25px row through a negative top margin.
|
||||
expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 35px')
|
||||
expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('margin: -10px calc(-1 * var(--dsh-tab-hit-inline)) 0')
|
||||
expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('padding: 10px var(--dsh-tab-hit-inline) 9px')
|
||||
})
|
||||
|
||||
it('uses 32px between view labels without changing the action gap', () => {
|
||||
|
||||
@@ -121,7 +121,7 @@ const EXTENSION_BUNDLES = [
|
||||
]
|
||||
/** The optional bundles the main list holds back until selected; the More list includes eligible entries. */
|
||||
const MORE_BUNDLES = [
|
||||
'@deepseek-ai/dsh-experimental-creation-recommendations',
|
||||
'@deepseek-ai/dsh-creation-recommendations',
|
||||
'@deepseek-ai/dsh-experimental-plugin-recommendations',
|
||||
'@deepseek-ai/dsh-experimental-inspector-profile',
|
||||
'@deepseek-ai/dsh-cua-bundle',
|
||||
|
||||
@@ -110,7 +110,7 @@ export function shortName(name: string): string {
|
||||
|
||||
/** Official bundles that display the Experimental badge. */
|
||||
const EXPERIMENTAL_BUNDLES: readonly string[] = [
|
||||
'@deepseek-ai/dsh-experimental-creation-recommendations',
|
||||
'@deepseek-ai/dsh-creation-recommendations',
|
||||
'@deepseek-ai/dsh-experimental-plugin-recommendations',
|
||||
'@deepseek-ai/dsh-cua-bundle',
|
||||
'@deepseek-ai/dsh-experimental-agent-team-profile',
|
||||
|
||||
@@ -1239,7 +1239,7 @@ describe('PluginManagerPage', () => {
|
||||
|
||||
it.each([
|
||||
'@deepseek-ai/dsh-experimental-session-titles-bundle',
|
||||
'@deepseek-ai/dsh-experimental-creation-recommendations',
|
||||
'@deepseek-ai/dsh-creation-recommendations',
|
||||
'@deepseek-ai/dsh-experimental-plugin-recommendations',
|
||||
])('returns %s details to the secondary page and retains it across React remounts', (name) => {
|
||||
const b = renderTab({ packages: [pkg({ name, official: true, enabled: false })] })
|
||||
@@ -1258,7 +1258,7 @@ describe('PluginManagerPage', () => {
|
||||
|
||||
it.each([
|
||||
'@deepseek-ai/dsh-experimental-tool-worktree',
|
||||
'@deepseek-ai/dsh-experimental-creation-recommendations',
|
||||
'@deepseek-ai/dsh-creation-recommendations',
|
||||
'@deepseek-ai/dsh-experimental-plugin-recommendations',
|
||||
])('pins enabled %s onto the main list and removes it when disabled', (first) => {
|
||||
const team = '@deepseek-ai/dsh-experimental-agent-team-profile'
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 3c79351ff2210ccb
|
||||
zh: 4cbe07ded89bbaa1
|
||||
/deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons:
|
||||
en: dbd7f0e74c45a762
|
||||
zh: 7efbfbf5500cc835
|
||||
en: b3744373b4b2c1fb
|
||||
zh: 2dbde8ee0bfa5f28
|
||||
/deepseek-ai-dsh-client-ui-primitives/use-this-package/rendering-agent-output:
|
||||
en: 02f310acbae29955
|
||||
zh: 1e41393d5d714b2d
|
||||
|
||||
@@ -96,9 +96,9 @@ Writing your own component in your own package is fine when the need is genuinel
|
||||
|
||||
### Controls and icons
|
||||
|
||||
The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions. Named components keep the chosen weight visible at import sites; the library owns stroke geometry instead of consumer CSS overrides. Each product icon renders an `<svg>` box with one `<use>` of its `dsw-icon-<glyph>-<weight>` symbol, for example `dsw-icon-search-outline-regular`, and names that id in `data-icon`. `ICON_SYMBOLS` holds every symbol's default SVG document; ui-theme renders the document icon sprite from it, and an icon whose symbol is missing renders nothing. Artwork reached through `<use>` inherits `currentColor` and CSS variables from the use site. Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic code glyph uses angle brackets and a slash at the supplied 1.35px stroke; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader in the same warning color as its label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; anchored overlays and portalled `Menu` add 20px to the frame's published top clearance, or keep 20px in native fullscreen, while preserving their own minimum margins in ordinary browsers. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Touch entry does not open its preview or intercept the anchor action. Mouse entry requires an available hover-capable pointer; keyboard focus remains independent of that capability. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minus 48px, centers the panel with 24px side insets, and places it above or below the row within the viewport. The panel keeps the frame’s top clearance, capsLine truncated
|
||||
The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions. Named components keep the chosen weight visible at import sites; the library owns stroke geometry instead of consumer CSS overrides. Each product icon renders an `<svg>` box with one `<use>` of its `dsw-icon-<glyph>-<weight>` symbol, for example `dsw-icon-search-outline-regular`, and names that id in `data-icon`. `ICON_SYMBOLS` holds every symbol's default SVG document; ui-theme renders the document icon sprite from it, and an icon whose symbol is missing renders nothing. Artwork reached through `<use>` inherits `currentColor` and CSS variables from the use site. Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic code glyph uses angle brackets and a slash at the supplied 1.35px stroke; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader in the same warning color as its label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; anchored overlays and portalled `Menu` add 20px to the frame's published top clearance, or keep 20px in native fullscreen, while preserving their own minimum margins in ordinary browsers; while an anchor is inside that top strip, its overlay, including a `HoverCard`, is never pushed lower than the anchor's bottom edge plus the overlay's gap. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Touch entry does not open its preview or intercept the anchor action. Mouse entry requires an available hover-capable pointer; keyboard focus remains independent of that capability. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minuLine truncated
|
||||
|
||||
`Tooltip` reads the anchor on hover or keyboard focus and fits its bubble from `ResizeObserver` border-box sizes. It stays hidden until its first fit, prefers the requested side, then tries the opposite and perpendicular sides, sliding along the anchor without covering it when a side fits. Horizontal placements keep a 10px gap; vertical placements use `gap` (8px by default). Width and height stay within a 12px viewport margin; text wraps, and content taller than the viewport scrolls when pinned. If no side fits, a stable viewport-bounded fallback can overlap the anchor. Viewport resize refreshes the anchor coordinates; label-size changes reuse them. Fitting does not synchronously measure the bubble or trigger a React render. Touch presses dismiss unpinned hints and do not trigger hover hints, including compatibility mouse events. Mouse entry opens hints only while `(any-hover: hover)` reports an available hover-capable input, excluding iOS mouse entry caused by a tapped icon changing. Keyboard focus and explicit click-to-open explanations remain available on touch-only devices. Opt-in `openOnClick` lets an informational button pin the bubble by click or tap and associates it with the anchor description. Another click or tap, Escape, Tab, or an outside pointerdown closes it. Ordinary action buttons execute their action directly without a touch tooltip.
|
||||
`Tooltip` reads the anchor on hover or keyboard focus and fits its bubble from `ResizeObserver` border-box sizes. It stays hidden until its first fit, prefers the requested side, then tries the opposite and perpendicular sides, sliding along the anchor without covering it when a side fits. Horizontal placements keep a 10px gap; vertical placements use `gap` (8px by default). Width and height stay within a 12px viewport margin; text wraps, and content taller than the viewport scrolls when pinned. If no side fits, a stable viewport-bounded fallback can overlap the anchor. Viewport resize refreshes the anchor coordinates; label-size changes reuse them. Fitting does not synchronously measure the bubble or trigger a React render. Touch presses dismiss unpinned hints and do not trigger hover hints, including compatibility mouse events. Mouse entry opens hints only while `(any-hover: hover)` reports an available hover-capable input, excluding iOS mouse entry caused by a tapped icon changing. Keyboard focus and explicit click-to-open explanations remain available on touch-only devices. Opt-in `openOnClick` lets an informational button pin the bubble by click or tap and associates it with the anchor description. Another click or tap, Escape, Tab, or an outside pointerdown closes it. Ordinary action buttons execute their action directly without a touch tooltip. Opt-in `onlyWhenClipped` shows the bubble only when the anchor or an HTML element inside it overflows horizontally when the bubble is about to show, so a label that repeats ellipsis-cut text appears only while it adds information.
|
||||
|
||||
<a id="rendering-agent-output"></a>
|
||||
|
||||
|
||||
@@ -96,9 +96,9 @@ kind: "package-library"
|
||||
|
||||
### 控件与图标
|
||||
|
||||
上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸。具名组件让所选线重在导入处可见;描边几何由组件库拥有,而不是由消费方 CSS 覆盖。每个产品图标渲染一个 `<svg>` 框,其中包含一个引用对应 `dsw-icon-<glyph>-<weight>` symbol 的 `<use>`,例如 `dsw-icon-search-outline-regular`,并在 `data-icon` 中写出该 id。`ICON_SYMBOLS` 保存每个 symbol 的默认 SVG 文档;ui-theme 据此渲染文档的图标 sprite,缺少对应 symbol 的图标不渲染任何内容。通过 `<use>` 引用的图形会从使用处继承 `currentColor` 与 CSS 变量。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用代码图形使用尖括号与斜线,保留原图 1.35px 描边;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、与文案使用相同警告色的共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;锚定浮层与 portal 模式的 `Menu` 在框架发布的顶部占用量上增加 20px,原生全屏时保留 20px,普通浏览器中沿用自身的最小边距。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。触摸进入不会打开预览,也不会拦截锚点操作。鼠标进入要求存在可悬停的指针设备;键盘聚焦不依赖该能力。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区,高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收指针输入,淡出后卸载。在淡出期间移回锚点可恢复显示。减少动态效果偏好会禁用过渡动画。只有启用复制时才必须提供复制标签。`HoveLine truncated
|
||||
上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸。具名组件让所选线重在导入处可见;描边几何由组件库拥有,而不是由消费方 CSS 覆盖。每个产品图标渲染一个 `<svg>` 框,其中包含一个引用对应 `dsw-icon-<glyph>-<weight>` symbol 的 `<use>`,例如 `dsw-icon-search-outline-regular`,并在 `data-icon` 中写出该 id。`ICON_SYMBOLS` 保存每个 symbol 的默认 SVG 文档;ui-theme 据此渲染文档的图标 sprite,缺少对应 symbol 的图标不渲染任何内容。通过 `<use>` 引用的图形会从使用处继承 `currentColor` 与 CSS 变量。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用代码图形使用尖括号与斜线,保留原图 1.35px 描边;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、与文案使用相同警告色的共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;锚定浮层与 portal 模式的 `Menu` 在框架发布的顶部占用量上增加 20px,原生全屏时保留 20px,普通浏览器中沿用自身的最小边距;锚点位于该顶部区域内时,其浮层(包括 `HoverCard`)不会被推到低于锚点底边加浮层间距的位置。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。触摸进入不会打开预览,也不会拦截锚点操作。鼠标进入要求存在可悬停的指针设备;键盘聚焦不依赖该能力。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区(位于顶部区域内的锚点下方时除外),高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收Line truncated
|
||||
|
||||
`Tooltip` 在悬停或键盘聚焦时读取锚点位置,再根据 `ResizeObserver` 提供的边框盒尺寸调整气泡。首次定位前保持隐藏,优先使用指定方向,再尝试相反方向和垂直于该方向的两侧;只要某一侧容得下,就沿锚点平移而不遮挡锚点。左右放置保留 10px 间距,上下放置使用 `gap`(默认 8px)。宽高限制在视口内并保留 12px 边距;文本自动换行,超出视口高度的内容在固定气泡后可滚动。若所有方向都放不下,则使用稳定的视口内回退位置,可能遮挡锚点。视口尺寸变化会重新读取锚点坐标,标签尺寸变化则复用坐标;定位不会同步测量气泡,也不会触发 React 渲染。 触摸按下会关闭未固定的提示,且不会触发悬停提示,包括触摸后的兼容鼠标事件。只有 `(any-hover: hover)` 表明存在可悬停的输入设备时,鼠标进入才会打开提示,从而排除 iOS 因已点击图标变化而生成的鼠标进入事件。仅有触摸输入的设备仍可使用键盘聚焦提示和显式点击打开的说明。信息按钮可显式启用 `openOnClick`,通过鼠标或触摸点击固定气泡供阅读,并将内容关联为锚点的无障碍描述;再次点击、Escape、Tab 或外部 pointerdown 会关闭。普通操作按钮在触摸点击时直接执行操作,不显示 Tooltip。
|
||||
`Tooltip` 在悬停或键盘聚焦时读取锚点位置,再根据 `ResizeObserver` 提供的边框盒尺寸调整气泡。首次定位前保持隐藏,优先使用指定方向,再尝试相反方向和垂直于该方向的两侧;只要某一侧容得下,就沿锚点平移而不遮挡锚点。左右放置保留 10px 间距,上下放置使用 `gap`(默认 8px)。宽高限制在视口内并保留 12px 边距;文本自动换行,超出视口高度的内容在固定气泡后可滚动。若所有方向都放不下,则使用稳定的视口内回退位置,可能遮挡锚点。视口尺寸变化会重新读取锚点坐标,标签尺寸变化则复用坐标;定位不会同步测量气泡,也不会触发 React 渲染。 触摸按下会关闭未固定的提示,且不会触发悬停提示,包括触摸后的兼容鼠标事件。只有 `(any-hover: hover)` 表明存在可悬停的输入设备时,鼠标进入才会打开提示,从而排除 iOS 因已点击图标变化而生成的鼠标进入事件。仅有触摸输入的设备仍可使用键盘聚焦提示和显式点击打开的说明。信息按钮可显式启用 `openOnClick`,通过鼠标或触摸点击固定气泡供阅读,并将内容关联为锚点的无障碍描述;再次点击、Escape、Tab 或外部 pointerdown 会关闭。普通操作按钮在触摸点击时直接执行操作,不显示 Tooltip。可选的 `onlyWhenClipped` 只在气泡即将显示时锚点或其内部某个 HTML 元素横向溢出才显示气泡,因此重复被省略号截断文本的标签只在能补充信息时出现。
|
||||
|
||||
<a id="rendering-agent-output"></a>
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ interface CardPosition {
|
||||
* readable and selectable, but it carries no dismissal affordance of its own.
|
||||
* @param props.openDelayMs - hover dwell before the card shows (default 500).
|
||||
* @param props.variant - compact card beside the anchor, or a preview above/below it
|
||||
* with 24px side insets, a 420px height cap, frame-top clearance, and 100ms opacity transitions.
|
||||
* with 24px side insets, a 420px height cap, the frame-top clearance (capped below an anchor inside the top strip),
|
||||
* and 100ms opacity transitions.
|
||||
* @param props.widthAnchorRef - optional element whose width and horizontal position size the preview.
|
||||
* @param props.inline - keep the anchor in prose; show a contained preview on hover or keyboard focus.
|
||||
* Its hover region connects the anchor and preview across the visual gap.
|
||||
@@ -178,7 +179,7 @@ export function HoverCard({
|
||||
if (variant === 'preview') {
|
||||
const bounds = widthAnchorRef?.current?.getBoundingClientRect() ?? r
|
||||
const width = Math.max(0, Math.min(bounds.width - PREVIEW_INSET * 2, window.innerWidth - VIEWPORT_MARGIN * 2))
|
||||
const topMargin = overlayTopMargin(VIEWPORT_MARGIN)
|
||||
const topMargin = overlayTopMargin(VIEWPORT_MARGIN, r, ANCHOR_GAP)
|
||||
const belowTop = Math.max(topMargin, r.bottom + ANCHOR_GAP)
|
||||
const above = Math.max(0, r.top - ANCHOR_GAP - topMargin)
|
||||
const below = Math.max(0, window.innerHeight - belowTop - VIEWPORT_MARGIN)
|
||||
@@ -195,7 +196,7 @@ export function HoverCard({
|
||||
const height = Math.max(h, cardRef.current?.scrollHeight ?? 0)
|
||||
const maxWidth = Math.max(0, Math.min(INLINE_PREVIEW_WIDTH, window.innerWidth - VIEWPORT_MARGIN * 2))
|
||||
const width = fitContent ? Math.min(cardRef.current?.offsetWidth ?? maxWidth, maxWidth) : maxWidth
|
||||
const topMargin = overlayTopMargin(VIEWPORT_MARGIN)
|
||||
const topMargin = overlayTopMargin(VIEWPORT_MARGIN, r, ANCHOR_GAP)
|
||||
const belowTop = Math.max(topMargin, r.bottom + ANCHOR_GAP)
|
||||
const above = Math.max(0, r.top - ANCHOR_GAP - topMargin)
|
||||
const below = Math.max(0, window.innerHeight - belowTop - VIEWPORT_MARGIN)
|
||||
|
||||
@@ -259,6 +259,7 @@ export function Menu({ open, anchor, items = [], children, selectedId, selectedI
|
||||
}
|
||||
if (r === null) return
|
||||
const MARGIN = 12
|
||||
const GAP = 4
|
||||
const vw = window.innerWidth
|
||||
const vh = window.innerHeight
|
||||
const listEl = listRef.current
|
||||
@@ -272,18 +273,18 @@ export function Menu({ open, anchor, items = [], children, selectedId, selectedI
|
||||
let x: number
|
||||
let y: number
|
||||
if (side === 'right') {
|
||||
x = r.right + 4
|
||||
x = r.right + GAP
|
||||
y = r.top
|
||||
} else if (align === 'start') {
|
||||
x = r.left
|
||||
y = side === 'bottom' ? r.bottom + 4 : r.top - lh - 4
|
||||
y = side === 'bottom' ? r.bottom + GAP : r.top - lh - GAP
|
||||
} else {
|
||||
x = r.right - lw
|
||||
y = side === 'bottom' ? r.bottom + 4 : r.top - lh - 4
|
||||
y = side === 'bottom' ? r.bottom + GAP : r.top - lh - GAP
|
||||
}
|
||||
|
||||
if (lw > 0) x = Math.min(Math.max(x, MARGIN), vw - lw - MARGIN)
|
||||
if (lh > 0) y = Math.min(Math.max(y, overlayTopMargin(MARGIN)), vh - lh - MARGIN)
|
||||
if (lh > 0) y = Math.min(Math.max(y, overlayTopMargin(MARGIN, r, GAP)), vh - lh - MARGIN)
|
||||
|
||||
setFixedPos(current => current?.left === x && current.top === y ? current : { left: x, top: y })
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ interface AnchorProps {
|
||||
|
||||
type TooltipLabel = string | (() => string)
|
||||
|
||||
function clipped(anchor: HTMLElement): boolean {
|
||||
return [anchor, ...anchor.querySelectorAll<HTMLElement>('*')]
|
||||
.some(el => el instanceof HTMLElement && el.scrollWidth > el.clientWidth)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a hover/focus tooltip to an anchor element.
|
||||
* @param props.label - bubble text, or a resolver evaluated only while visible; an empty string shows only shortcut keys.
|
||||
@@ -54,6 +59,9 @@ type TooltipLabel = string | (() => string)
|
||||
* half-viewport cap would render a slab wider than the surface the anchor sits on; always bounded by viewport margins.
|
||||
* @param props.openOnClick - clicking or tapping also pins the bubble for reading; another click, Escape,
|
||||
* Tab, or an outside pointerdown dismisses it. Defaults to false for ordinary action tooltips.
|
||||
* @param props.onlyWhenClipped - show the bubble only when the anchor or an HTML element inside it
|
||||
* overflows horizontally, measured when the bubble is about to show; for labels that repeat
|
||||
* text an ellipsis may cut off. Defaults to false.
|
||||
* @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's.
|
||||
* @returns the cloned anchor plus a fixed-position bubble, optionally portaled to the body.
|
||||
* The bubble stays hidden until ResizeObserver supplies its size for viewport fitting; clicking the
|
||||
@@ -62,7 +70,7 @@ type TooltipLabel = string | (() => string)
|
||||
* tooltips; tapping still invokes the child's action and can open an openOnClick explanation.
|
||||
* When no side fits, the viewport-bounded fallback may overlap the anchor; oversized content is scrollable when pinned.
|
||||
*/
|
||||
export function Tooltip({ label, shortcutKeys, side = 'right', align = 'center', delayMs = 0, focusDelayMs = 0, gap = 8, disabled = false, portal = false, maxWidth, openOnClick = false, children }: { label: TooltipLabel; shortcutKeys?: readonly string[] | undefined; side?: TooltipSide; align?: 'center' | 'end'; delayMs?: number; focusDelayMs?: number; gap?: number; disabled?: boolean; portal?: boolean; maxWidth?: number; openOnClick?: boolean; children: ReactElement<AnchorProps> }) {
|
||||
export function Tooltip({ label, shortcutKeys, side = 'right', align = 'center', delayMs = 0, focusDelayMs = 0, gap = 8, disabled = false, portal = false, maxWidth, openOnClick = false, onlyWhenClipped = false, children }: { label: TooltipLabel; shortcutKeys?: readonly string[] | undefined; side?: TooltipSide; align?: 'center' | 'end'; delayMs?: number; focusDelayMs?: number; gap?: number; disabled?: boolean; portal?: boolean; maxWidth?: number; openOnClick?: boolean; onlyWhenClipped?: boolean; children: ReactElement<AnchorProps> }) {
|
||||
const id = useId()
|
||||
const [pinned, setPinned] = useState(false)
|
||||
const anchor = useRef<HTMLElement | null>(null)
|
||||
@@ -156,6 +164,7 @@ export function Tooltip({ label, shortcutKeys, side = 'right', align = 'center',
|
||||
const el = anchor.current
|
||||
/* v8 ignore next -- the ref is attached by event time: events fire on the cloned anchor. */
|
||||
if (el === null) return
|
||||
if (onlyWhenClipped && !clipped(el)) return
|
||||
setPos(el.getBoundingClientRect())
|
||||
announce(true)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
/**
|
||||
* Resolve the top margin an overlay keeps from the viewport edge.
|
||||
* @param min - the overlay's own viewport margin in px, used as the floor.
|
||||
* @returns the larger of `min` and the frame clearance plus 20px; fullscreen keeps only the 20px gap.
|
||||
* @param anchor - the overlay's anchor rect in viewport coordinates. While the anchor's top edge is inside the
|
||||
* viewport, the margin never exceeds `anchor.bottom + gap`, so an overlay opened from a control inside the top strip
|
||||
* stays next to it.
|
||||
* @param gap - the distance the overlay keeps below `anchor`.
|
||||
* @returns the frame clearance plus 20px (20px in fullscreen), capped by a visible `anchor`, but never below `min`.
|
||||
*/
|
||||
export function overlayTopMargin(min: number): number {
|
||||
export function overlayTopMargin(min: number, anchor?: { readonly top: number; readonly bottom: number }, gap = 0): number {
|
||||
const root = document.documentElement
|
||||
const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue('--dsh-frame-top-clearance'))
|
||||
if (Number.isNaN(clearance)) return min
|
||||
return Math.max(min, (root.hasAttribute('data-fullscreen') ? 0 : clearance) + 20)
|
||||
const cap = anchor !== undefined && anchor.top >= 0 ? anchor.bottom + gap : Number.POSITIVE_INFINITY
|
||||
return Math.max(min, Math.min((root.hasAttribute('data-fullscreen') ? 0 : clearance) + 20, cap))
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export function useAnchoredPosition(options: AnchoredPositionOptions): CSSProper
|
||||
let left = align === 'end' ? rect.right - width : rect.left
|
||||
let top = side === 'top' ? rect.top - gap - height : rect.bottom + gap
|
||||
if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin)
|
||||
if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin)
|
||||
if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin, rect, gap)), window.innerHeight - height - margin)
|
||||
/* v8 ignore stop */
|
||||
setPosition({ left, top })
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ describe('HoverCard', () => {
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps previews below the desktop frame clearance', () => {
|
||||
it('keeps previews clear of the frame top strip except below an anchor inside it', () => {
|
||||
const prior = document.documentElement.style.getPropertyValue('--dsh-frame-top-clearance')
|
||||
const priority = document.documentElement.style.getPropertyPriority('--dsh-frame-top-clearance')
|
||||
document.documentElement.style.setProperty('--dsh-frame-top-clearance', '100px')
|
||||
@@ -258,6 +258,10 @@ describe('HoverCard', () => {
|
||||
fireEvent(window, new Event('resize'))
|
||||
expect(card.style.top).toBe('120px')
|
||||
expect(card.style.maxHeight).toBe('322px')
|
||||
// An anchor inside the top strip keeps its preview directly below it.
|
||||
wrapper.getBoundingClientRect = () => DOMRect.fromRect({ x: 100, y: 10, width: 600, height: 32 })
|
||||
fireEvent(window, new Event('resize'))
|
||||
expect(card.style.top).toBe('50px')
|
||||
} finally {
|
||||
if (prior === '') document.documentElement.style.removeProperty('--dsh-frame-top-clearance')
|
||||
else document.documentElement.style.setProperty('--dsh-frame-top-clearance', prior, priority)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// @vitest-environment jsdom
|
||||
/** Portal menus keep the desktop frame's top clearance without leaving a gap below an anchor inside it. */
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { Menu } from '../src/Menu.tsx'
|
||||
|
||||
const root = document.documentElement
|
||||
let prior = ''
|
||||
|
||||
beforeEach(() => {
|
||||
prior = root.style.getPropertyValue('--dsh-frame-top-clearance')
|
||||
root.style.setProperty('--dsh-frame-top-clearance', '48px')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.restoreAllMocks()
|
||||
if (prior === '') root.style.removeProperty('--dsh-frame-top-clearance')
|
||||
else root.style.setProperty('--dsh-frame-top-clearance', prior)
|
||||
})
|
||||
|
||||
/**
|
||||
* Open an 80px-tall portal menu from an anchor with the stated rect.
|
||||
* @param anchor - the anchor rect in viewport coordinates.
|
||||
* @param side - which anchor edge the menu opens from.
|
||||
* @returns the render result.
|
||||
*/
|
||||
function openMenu(anchor: DOMRect, side: 'top' | 'bottom') {
|
||||
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue(anchor)
|
||||
vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(120)
|
||||
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(80)
|
||||
return render(<Menu open portal side={side} anchor={<button>Open</button>} items={[{ id: 'one', label: 'One' }]} onClose={vi.fn()} />)
|
||||
}
|
||||
|
||||
describe('Menu top clearance', () => {
|
||||
it('opens directly below an anchor that sits inside the top strip', () => {
|
||||
const view = openMenu(new DOMRect(300, 14, 18, 22), 'bottom')
|
||||
expect(view.getByRole('menu').style.top).toBe('40px')
|
||||
})
|
||||
|
||||
it('keeps a menu that does not fit above its anchor below the top strip', () => {
|
||||
const view = openMenu(new DOMRect(300, 100, 18, 22), 'top')
|
||||
expect(view.getByRole('menu').style.top).toBe('68px')
|
||||
})
|
||||
|
||||
it('places a menu that does not fit above an anchor inside the top strip directly below it', () => {
|
||||
const view = openMenu(new DOMRect(300, 14, 18, 22), 'top')
|
||||
expect(view.getByRole('menu').style.top).toBe('40px')
|
||||
})
|
||||
|
||||
it('keeps the top strip clear when the anchor has scrolled above the viewport', () => {
|
||||
const view = openMenu(new DOMRect(300, -30, 18, 22), 'bottom')
|
||||
expect(view.getByRole('menu').style.top).toBe('68px')
|
||||
})
|
||||
})
|
||||
@@ -765,6 +765,30 @@ describe('Tooltip', () => {
|
||||
expect(screen.getByRole('tooltip')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows a clipped-only bubble when the anchor or a descendant overflows at show time', () => {
|
||||
render(
|
||||
<Tooltip label="Full label" onlyWhenClipped>
|
||||
<button type="button"><svg /><span>Full…</span></button>
|
||||
</Tooltip>,
|
||||
)
|
||||
const anchor = screen.getByRole('button')
|
||||
const label = screen.getByText('Full…')
|
||||
const overflowing = new Set<Element>()
|
||||
vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockImplementation(function (this: Element) { return overflowing.has(this) ? 120 : 80 })
|
||||
vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(80)
|
||||
fireEvent.pointerEnter(anchor)
|
||||
expect(screen.queryByRole('tooltip')).toBeNull()
|
||||
fireEvent.pointerLeave(anchor)
|
||||
overflowing.add(label)
|
||||
fireEvent.pointerEnter(anchor)
|
||||
expect(screen.getByRole('tooltip').textContent).toBe('Full label')
|
||||
fireEvent.pointerLeave(anchor)
|
||||
overflowing.clear()
|
||||
overflowing.add(anchor)
|
||||
fireEvent.pointerEnter(anchor)
|
||||
expect(screen.getByRole('tooltip').textContent).toBe('Full label')
|
||||
})
|
||||
|
||||
it('mouse leave hides the bubble immediately, even while the anchor stays focused', () => {
|
||||
render(
|
||||
<Tooltip label="Sticky">
|
||||
|
||||
@@ -51,10 +51,12 @@ function stubResizeObserver(): Recorded[] {
|
||||
* @param props - whether the panel is open.
|
||||
* @returns the anchor and, while open, the panel carrying the position.
|
||||
*/
|
||||
function Host({ open, align }: { open: boolean; align?: 'start' | 'end' }) {
|
||||
function Host({ open, align, side }: { open: boolean; align?: 'start' | 'end'; side?: 'top' | 'bottom' }) {
|
||||
const anchorRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
const position = useAnchoredPosition({ open, anchorRef, panelRef, gap: 4, margin: 12, ...align === undefined ? {} : { align } })
|
||||
const position = useAnchoredPosition({
|
||||
open, anchorRef, panelRef, gap: 4, margin: 12, ...align === undefined ? {} : { align }, ...side === undefined ? {} : { side },
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<button ref={anchorRef} type="button">anchor</button>
|
||||
@@ -64,7 +66,11 @@ function Host({ open, align }: { open: boolean; align?: 'start' | 'end' }) {
|
||||
}
|
||||
|
||||
describe('useAnchoredPosition', () => {
|
||||
it.each([{ fullscreen: false, top: 60 }, { fullscreen: true, top: 20 }])('keeps an anchored panel at $top with fullscreen=$fullscreen', ({ fullscreen, top }) => {
|
||||
it.each([
|
||||
{ name: 'a panel that does not fit above its anchor', side: 'top', anchor: new DOMRect(100, 100, 200, 10), fullscreen: false, top: 60 },
|
||||
{ name: 'a panel that does not fit above its anchor', side: 'top', anchor: new DOMRect(100, 100, 200, 10), fullscreen: true, top: 20 },
|
||||
{ name: 'a panel below an anchor inside the top strip', side: 'bottom', anchor: new DOMRect(100, 0, 200, 10), fullscreen: false, top: 14 },
|
||||
] as const)('keeps $name at $top with fullscreen=$fullscreen', ({ side, anchor, fullscreen, top }) => {
|
||||
const root = document.documentElement
|
||||
const previous = root.getAttribute('style')
|
||||
const priorFullscreen = root.getAttribute('data-fullscreen')
|
||||
@@ -77,10 +83,10 @@ describe('useAnchoredPosition', () => {
|
||||
})
|
||||
root.style.setProperty('--dsh-frame-top-clearance', '40px')
|
||||
root.toggleAttribute('data-fullscreen', fullscreen)
|
||||
vi.spyOn(HTMLButtonElement.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(100, 0, 200, 10))
|
||||
vi.spyOn(HTMLButtonElement.prototype, 'getBoundingClientRect').mockReturnValue(anchor)
|
||||
vi.spyOn(HTMLDivElement.prototype, 'offsetHeight', 'get').mockReturnValue(400)
|
||||
vi.stubGlobal('innerHeight', 600)
|
||||
const { getByTestId } = render(<Host open />)
|
||||
const { getByTestId } = render(<Host open side={side} />)
|
||||
expect(getByTestId('panel').style.top).toBe(`${top}px`)
|
||||
})
|
||||
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
zh: eb3e5d5a802da592
|
||||
/deepseek-ai-dsh-client-ui-schedule/summary:
|
||||
en: 8dfc371c14bb4415
|
||||
zh: 9b3545c1a7468a7a
|
||||
zh: 5e2fa588983524ed
|
||||
/deepseek-ai-dsh-client-ui-schedule/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: 405b30feaf48c087
|
||||
/deepseek-ai-dsh-client-ui-schedule/use-this-package:
|
||||
en: babac791422f1b8f
|
||||
zh: f633dbfc1b7bb4bd
|
||||
en: fccffd577fcf93f2
|
||||
zh: 4719236f7d89e0a8
|
||||
/deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source:
|
||||
en: 47cd16c142e82385
|
||||
zh: 181849a3c7a167fd
|
||||
/deepseek-ai-dsh-client-ui-schedule/understand-the-implementation:
|
||||
en: 959145f9a08c3913
|
||||
zh: caae642a82d2acd3
|
||||
zh: 7c0afb742790c474
|
||||
/deepseek-ai-dsh-client-ui-schedule/further-exploration:
|
||||
en: 06e265120b05dbf5
|
||||
zh: 08ab1ef979b8ea40
|
||||
|
||||
@@ -25,9 +25,9 @@ Manage active and inactive tasks from the Automation tasks page and the right Si
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation.
|
||||
The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Once the catalog holds an inactive task, the search row's More task actions menu offers Clear all inactive tasks, which removes every inactive task after a confirmation that states how many inactive tasks the catalog currently holds. The page's task list supports text search and one Enabled, Inactive, and All status filter row, starting on Enabled; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation.
|
||||
|
||||
The page header keeps the title at its natural width and moves New to another line when the two no longer fit together. The action label stays on one line. On a coarse pointer the list's search box and the detail's instruction and run-time fields take a 16px step — iOS Safari zooms the visual viewport for a focused field below that size — while the name control keeps its 20px step and a fine pointer keeps the compact steps.
|
||||
The page header keeps the title at its natural width and moves the actions to another line when the title and the actions no longer fit together. Each action label stays on one line. On a coarse pointer the list's search box and the detail's instruction and run-time fields take a 16px step — iOS Safari zooms the visual viewport for a focused field below that size — while the name control keeps its 20px step and a fine pointer keeps the compact steps.
|
||||
|
||||
Selecting a task from the list opens Rules; refreshing that task preserves the selected tab. Rules edits an active task's name, instruction, and run time; the edits stay in a local draft until saved, and inactive tasks remain read-only. Unmodified Left/Right arrows cycle the tabs, Home/End select the first/last tab, and keyboard focus follows the selection. The tabs do not consume these keys with Alt, Ctrl, Meta, or Shift. The linked-Session control in the detail tab strip stays available on the Rules tab, and confirmed deletion is available from either view.
|
||||
|
||||
@@ -41,7 +41,7 @@ After the last saved page loads, a nonempty history with confirmed pruning shows
|
||||
|
||||
A successful empty history page is distinct from loading, query failure, a missing task, or an invalid cursor. Failed requests retain previously loaded records with a warning and Retry delivery records; an invalid cursor offers Refresh delivery records to reload the newest page. Changing the latest `messageId` refreshes the newest page without switching tabs. Responses superseded by a refresh or received after changing tasks, leaving Records, or closing details do not replace the current view.
|
||||
|
||||
The Automation tasks page requires confirmation before deleting a task. Deletion is hard: the Host removes the stored task row together with its saved delivery records, so the task stops future delivery, leaves `list` and `catalog`, and its delivery history is no longer readable. Deletion leaves the original Session and queued messages intact, and the list keeps the row visible only until an authoritative query confirms the removal. Every deletion path — the Tasks page detail, the Session task tab, and the header popover — reports its settled outcome into one store that the app-wide `shell.overlay` notice renders, so the outcome outlives the panel or tab that asked for it: the notice reads `Task deleted.` once the Host removed the row with its saved delivery records, and `Could not delete the task.` after a failure, which leaves the rule in place to retry. Confirming a deletion closes the detail or the tab it was confirmed from, and each notice is keyed by its own sequence so a later one restarts the banner instead of being folded into an earlier one. A query failure keeps the last known records and shows a separate Retry action; it never appears as an empty successful result. A one-shot remains stored as inactive after durable inbox delivery. Its details and original Session id remain available through the All or Inactive filter until explicit deletion. The latest delivery shows its occurrence time; the message id and delivery acknowledgment time are not shown. Neither inactive status nor that receipt means model execution completed.
|
||||
The Automation tasks page requires confirmation before deleting a task. Deletion is hard: the Host removes the stored task row together with its saved delivery records, so the task stops future delivery, leaves `list` and `catalog`, and its delivery history is no longer readable. Deletion leaves the original Session and queued messages intact, and the list keeps the row visible only until an authoritative query confirms the removal. Every deletion path — the Tasks page detail, the Session task tab, the header popover, and the Tasks page's bulk cleanup — reports its settled outcome into one store that the app-wide `shell.overlay` notice renders, so the outcome outlives the panel or tab that asked for it: the notice reads `Task deleted.` once the Host removed the row with its saved delivery records, and `Could not delete the task.` after a failure, which leaves the rule in place to retry. The same notice reports a bulk cleanup: `Cleared N inactive tasks.` with the count the Host answered, `No inactive tasks to clear.` when the Host reports that it removed none, and `Could not clear tasks.` after a failure; a normal failure leaves the remaining inactive rows for a retry, while a backend `unknown-state` failure may have removed some rows and quarantines the Host's schedule domain until it is reopened. The bulk cleanup closes only its confirmation, whereas confirming a single deletion closes the detail or the tab it was confirmed from. Each notice is keyed by its own sequence so a later one restarts the banner instead of being folded into an earlier one. A query failure keeps the last known records and shows a separate Retry action; it never appears as an empty successful result. A one-shot remains stored as inactive after durable inbox delivery. Its details and original Session id remain available through the All or Inactive filter until explicit deletion. The latest delivery shows its occurrence time; the message id and delivery acknowledgment time are not shown. Neither inactive status nor that receipt means model execution completed.
|
||||
|
||||
An open Session renders an icon-only clock button among its header's right-aligned utilities, immediately left of the overflow menu, only once it has something to open: the first read mounts nothing, a read that returns no active reminder leaves the header without the button, a refresh keeps the last known reminders visible, and a failed read keeps the button with its retry. That rule prevents the clock from appearing and disappearing around the first answer. Its accessible name states the reminder count when the read is ready and the generic reminder label otherwise. With exactly one active reminder, the button opens that task's detail in the right Sidebar directly. With any other count it opens a popover that lists overdue reminders first, then future reminders by target time, with stored title, frequency, browser-local target time, relative time, and a Delete button; the title is a button that opens that task's detail in the right Sidebar. The task tab names the shown task's stored title on its chip and carries the same rule, delivery records, name, instruction, and run-time editing, confirmed deletion, and original-Session link as the Automation tasks page; it never activates a Session. Daily frequency text retains the saved local time and zone, and every next-target instant is shown as a device-zone clock with its relative duration in parentheses; the rule's own zone appears only in that frequency text. Escape closes the popover and returns focus to the button; an outside pointer press dismisses it. Deleting the final reminder removes the button, and an already open popover stays expandable until then.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-reference"
|
||||
|
||||
## 概述
|
||||
|
||||
在「自动化任务」页面和右侧栏的任务页签中管理各个会话的活动和未运行定时任务:搜索、筛选、修改名称、指令和运行时间、浏览任务运行记录、删除任务,或打开原会话。每个界面都用任务已存储的标题命名任务。已打开会话在存在活动提醒时,页头显示仅图标的提醒时钟,点击在右侧栏打开该任务详情;`schedule_create` 调用把创建的任务渲染为会话卡片;侧栏中带活动任务的未归档空闲会话行显示时钟标记,其悬停卡片列出这些任务。
|
||||
在「自动化任务」页面和右侧栏的任务页签中管理各个会话的活动和已结束定时任务:搜索、筛选、修改名称、指令和运行时间、浏览任务运行记录、删除任务,或打开原会话。每个界面都用任务已存储的标题命名任务。已打开会话在存在活动提醒时,页头显示仅图标的提醒时钟,点击在右侧栏打开该任务详情;`schedule_create` 调用把创建的任务渲染为会话卡片;侧栏中带活动任务的未归档空闲会话行显示时钟标记,其悬停卡片列出这些任务。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -25,15 +25,15 @@ kind: "package-reference"
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。
|
||||
随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。目录中存在已结束任务后,搜索行右侧的「更多任务操作」菜单提供「清除全部已结束任务」,经确认后移除全部已结束任务,确认提示说明目录当前包含多少个已结束任务。列表支持文本搜索和一行“已开启/已结束/全部”状态筛选,默认停在“已开启”;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或已结束任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。
|
||||
|
||||
页头按标题的自然宽度排布;标题和“新建”无法并排容纳时,将按钮移到下一行。操作文案保持单行。粗指针设备上,列表的搜索框以及详情的指令和运行时间输入框采用 16px 字号——iOS Safari 会放大聚焦到更小字号输入框时的视觉视口——而名称输入框保留 20px、细指针设备保留紧凑字号。
|
||||
页头按标题的自然宽度排布;标题和操作无法并排容纳时,将操作移到下一行。每个操作文案保持单行。粗指针设备上,列表的搜索框以及详情的指令和运行时间输入框采用 16px 字号——iOS Safari 会放大聚焦到更小字号输入框时的视觉视口——而名称输入框保留 20px、细指针设备保留紧凑字号。
|
||||
|
||||
从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,未运行任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。
|
||||
从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,已结束任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。
|
||||
|
||||
“规则”在“运行时间”卡片中以行编辑本地草稿:“重复”是含每周、周一至周五、每天、每 N 小时、每 N 分钟、每 N 秒、仅一次和自定义(cron)的菜单,每周规则增加“星期”行,固定速率选项只修改它所属单位的间隔数量,一次性规则用独立的“日期”和“时间”两行编辑目标,自定义(cron)用一行“Cron 表达式”编辑按所选时区解释的 5 字段表达式。卡片在浏览器中解析该表达式,并在该行下方显示描述它的本地化语句;Host 会拒绝的表达式改为显示本地化校验信息,并在解析通过前阻止保存。已存储的每周规则若星期集合恰好为周一至周五,则选中“周一至周五”选项。“日期”行是只读触发器,以 `YYYY/MM/DD` 显示暂存的日期并展开带本地化星期表头、月份切换和今天标记的月历;草稿与提交文本保持 ISO 形态;“时间”行是只读触发器,显示统一的 24 小时时钟,并展开时、分、秒三列可滚动列表,当前值高亮并在打开时滚动到可见。在月历或时钟上点选即按与其他行相同的分阶段保存写入草稿;方向键在打开的列或网格内移动,Enter 选定当前值,Escape 关闭面板并把焦点还给所在行,被禁用的行无法打开面板。时钟行显示到秒,未改动的行在提交时保留已存储的毫秒值,编辑时间时按整秒精度提交。“时区”使用高度受限的可搜索菜单。桌面端打开时自动聚焦搜索框;手机和平板需要手动聚焦。顶部无标题区域保留浏览器本地最近选过的 5 个准确时区;首次使用按真实系统时区和 UTC 初始化,而不根据界面语言猜测所在地。系统时区带有标记,其余清单按当前 UTC 偏移、再按规范 IANA 标识排序。可见文案把 `UTC±HH:MM` 放在本地化时区名称之前;IANA 标识仅在内部使用,但可用于搜索。ICU 显示成相同偏移和本地化名称的多个时区只占一行;所有被合并的标识仍可用于搜索,任务已保存的标识也会继续作为选中值,不会被改写。时区清单来自 `Intl.supportedValuesOf('timeZone')`,名称来自 `Intl.DateTimeFormat(..., { timeZoneName: 'longGeneric' })`:两者都是浏览器内置的 ICU/CLDR 数据,不是抓取或产品自维护的翻译表。因此没有需要刷新的爬虫或生成时区资源;第三方语言包只需提供常规的 `time.locale` 值,即可自动获得对应的 ICU 名称,仅需翻译选择器周边的 UI 文案。运行时无法枚举时改用简短的标识候选列表,已存储但不在其中的时区会保留。每天、每周和 cron 保留各自已存储的规则与时区,其频率行仅在时区与宿主系统时区不同时标出时区。一次性 After/At 以记录已存储的时区初始化日期和时间;记录未保存时区时取本设备时区,初始钟表值在该时区下仍表示同一瞬时;更改时区会保留输入的钟表值并改变时点。因选择种类而重新初始化的种类使用已提交发生时点的钟表值和该选项的时区,该时区取记录已存储的时区或本设备时区:每周的星期取自该发生时点在该时区下的星期,cron 则把该发生时点的钟表值写成每天执行的表达式。固定间隔至少为 60 整秒,不受时区影响。一次保存以分阶段的变更提交完整预期记录,并保留 id、原会话、最近一次回执及已保存历史;未来目标选择、重新确定起算时间和未变更规则的无写入语义见 [Schedule](../../schedule/schedule/README.zh.md#use-this-package)。
|
||||
|
||||
行在保存进行中、目录加载中、删除期间以及任务未运行时禁用。仅当本地草稿与已存储任务不同时才出现保存栏:显示“有未保存的修改”及“取消”和“保存修改”,保存期间“保存修改”变为“保存中…”。“取消”恢复已存储的值。仅当草稿没有未保存修改时,目录刷新才会成为显示值,因此未保存的草稿会保留到权威刷新之后。保存失败保留草稿并显示本地化提示;冲突后针对刷新后的记录重试。任务从目录消失时,“删除”会禁用,直到任务重新出现。更新成功后刷新权威目录。更新被拒绝或无法确认与目录读取失败相互区分:“重试”重新加载目录,不将该更新显示为失败。若更新本身无法确认,应在重试前检查任务。切换任务或关闭详情后才到达的响应不会替换当前视图,任何失败都不会回滚宿主已开始的写入。
|
||||
行在保存进行中、目录加载中、删除期间以及任务已结束时禁用。仅当本地草稿与已存储任务不同时才出现保存栏:显示“有未保存的修改”及“取消”和“保存修改”,保存期间“保存修改”变为“保存中…”。“取消”恢复已存储的值。仅当草稿没有未保存修改时,目录刷新才会成为显示值,因此未保存的草稿会保留到权威刷新之后。保存失败保留草稿并显示本地化提示;冲突后针对刷新后的记录重试。任务从目录消失时,“删除”会禁用,直到任务重新出现。更新成功后刷新权威目录。更新被拒绝或无法确认与目录读取失败相互区分:“重试”重新加载目录,不将该更新显示为失败。若更新本身无法确认,应在重试前检查任务。切换任务或关闭详情后才到达的响应不会替换当前视图,任何失败都不会回滚宿主已开始的写入。
|
||||
|
||||
打开“任务运行记录”时才请求最新的 20 条已保存记录。“加载更早的记录”按追加顺序从新到旧增加一页,不受实际时间回拨影响。该视图中页签条就是整个页头:名称输入框和“下次运行”行只属于“规则”。每条“已保存的任务运行记录”以时钟图标和任务时区下的本地月份名与时钟字段开头(一次性记录使用浏览器时区),并在存在时显示实际保存的提示文本。超过两行的提示文本只显示前两行并提供“展开”,展开后可“收起”;面板宽度变化时,收起状态的提示文本会重新判断是否超过两行。旧回执没有提示文本时,绝不以当前指令代替。
|
||||
|
||||
@@ -41,7 +41,7 @@ kind: "package-reference"
|
||||
|
||||
历史查询成功的空页与加载中、查询失败、任务不存在或游标无效相互区分。请求失败时保留此前加载的记录,并显示警告和“重试加载任务运行记录”;游标无效时提供“刷新任务运行记录”,重新加载最新一页。最新 `messageId` 变化时刷新最新一页,不切换页签。被刷新取代的响应,以及切换任务、离开任务运行记录或关闭详情后才收到的响应,不会替换当前视图。
|
||||
|
||||
自动化任务页面要求确认后才能删除任务。删除是硬删除:Host 连同已保存的任务运行记录一起移除存储的任务行,该任务因此停止后续投递、离开 `list` 与 `catalog`,其投递历史不再可读。删除保留原会话和已入队消息;列表仅在权威查询确认移除之前保留该行。每条删除路径——自动化任务页的详情、会话的任务页签、以及页头弹层——都把最终结果写入同一个存储,由应用级 `shell.overlay` 提示渲染,因此提示的生命周期长于发起它的面板或页签:Host 连同已保存的运行记录移除该行后提示显示「任务已删除」,失败时显示「无法删除任务」,并保留规则以便重试。确认删除会关闭确认所在的面板或页签,每条提示按自身序号区分,后一条会重新开始横幅而不是并入前一条。查询失败保留最近已知记录,并显示独立的重试操作;不会呈现为查询成功的空结果。一次性任务在持久收件箱投递后仍以未运行状态保存。显式删除前,通过全部或未运行筛选仍可查看详情与原会话 id。最近一次投递显示其发生时间,不显示消息 id 和投递确认时间;未运行状态和该回执均不表示模型执行完成。
|
||||
自动化任务页面要求确认后才能删除任务。删除是硬删除:Host 连同已保存的任务运行记录一起移除存储的任务行,该任务因此停止后续投递、离开 `list` 与 `catalog`,其投递历史不再可读。删除保留原会话和已入队消息;列表仅在权威查询确认移除之前保留该行。每条删除路径——自动化任务页的详情、会话的任务页签、以及页头弹层——都把最终结果写入同一个存储,由应用级 `shell.overlay` 提示渲染,因此提示的生命周期长于发起它的面板或页签:Host 连同已保存的运行记录移除该行后提示显示「任务已删除」,失败时显示「无法删除任务」,并保留规则以便重试。批量清理也由该提示报告:显示 Host 答复的行数「已清理 N 个已结束任务」;Host 答复未移除任何行时显示「没有可清理的已结束任务」;失败时显示「清除任务失败」;普通失败会保留尚未移除的已结束行以便重试,后端报告 `unknown-state` 时可能已移除部分行,并会隔离 Host 的 schedule 领域,直到重新打开。批量清理只关闭自己的确认对话框。确认删除会关闭确认所在的面板或页签,每条提示按自身序号区分,后一条会重新开始横幅而不是并入前一条。查询失败保留最近已知记录,并显示独立的重试操作;不会呈现为查询成功的空结果。一次性任务在持久收件箱投递后仍以已结束状态保存。显式删除前,通过全部或已结束筛选仍可查看详情与原会话 id。最近一次投递显示其发生时间,不显示消息 id 和投递确认时间;已结束状态和该回执均不表示模型执行完成。
|
||||
|
||||
已打开的会话只有在确实有可打开的内容时,才在页头右侧功能位中、紧邻更多操作菜单左侧渲染一个仅图标时钟按钮:首次读取前不渲染任何内容,读取结果为没有活动提醒时页头不显示该按钮,刷新期间保留上一次已知的提醒,读取失败则保留按钮及其重试。该规则避免时钟在首次结果前后出现又消失。其无障碍名称在读取就绪时给出提醒数量,否则显示通用提醒标签。当该会话只有一个活动提醒时,按钮直接在右侧栏打开该任务的详情。其余数量都会打开弹层:先列出逾期提醒,再按目标时间列出未来提醒,显示已存储的标题、频率、浏览器本地目标时间、相对时间及删除按钮;标题本身是一个按钮,点击后在右侧栏打开该任务的详情。任务页签在标签上显示当前任务已存储的标题,并提供与自动化任务页面相同的规则、任务运行记录、名称、指令和运行时间修改、确认删除和原会话入口;它不会激活会话。每日频率文本保留保存的本地时间和时区;每个下一目标时点都显示为设备时区的钟点和括号内的相对时长;规则自身的时区只显示在该频率文本中。Escape 关闭弹层并将焦点返回按钮;点击外部将其关闭。删除最后一条提醒会移除按钮;在此之前,已经打开的弹层保持可展开状态。
|
||||
|
||||
@@ -49,7 +49,7 @@ kind: "package-reference"
|
||||
|
||||
调用 `schedule_create` 时,创建的任务会以卡片形式渲染在 transcript 中,包含时钟图标、已存储的标题、频率,以及打开该任务右侧栏页签的“打开”按钮。卡片先按该调用持久化的结果 JSON 收窄出任务,并从该结果读取 `title`;当一次在卡片出现之后成功结束的目录读取解析到该任务时,卡片改为跟随目录显示当前名称与频率,若该读取中不含该记录,则以“已删除”代替频率显示。在已存储字段存在之前录制的结果回退为指令首行。仍在运行的调用、创建失败,或结果中没有完整任务时,卡片保留该行,并在结果为单个非空文本块时显示原始结果文本,不提供“打开”操作。卡片不增加 Host 字段,也不改变 Session 日志格式,因此早于该卡片录制的 Session 也会渲染它。
|
||||
|
||||
侧栏的 Session 行会标记有定时任务的会话:当某行处于空闲状态、未归档且其会话至少有一个活动任务时,该行显示时钟标记;归档行该格留空,其实时状态只在悬浮卡片中显示。对存在活动任务的会话长按悬停时,无论该行是否空闲,都会在其已有的悬浮卡片内增加自动化区段,最多列出两个任务(图标、已存储的标题、频率与下次运行文本),还有更多任务被隐藏时显示遗漏行。处于更高优先级状态的行——有待处理交互、活动仍在进行,或有未查看的完成提醒——保留自身状态点且不渲染标记,两者不会同时出现。该标记和悬停区段读取共享的活动与未运行 Host 任务目录,并从中筛选本会话的活动任务;它们不会激活、保留或取消归档任何会话,也不读取 Session 日志。
|
||||
侧栏的 Session 行会标记有定时任务的会话:当某行处于空闲状态、未归档且其会话至少有一个活动任务时,该行显示时钟标记;归档行该格留空,其实时状态只在悬浮卡片中显示。对存在活动任务的会话长按悬停时,无论该行是否空闲,都会在其已有的悬浮卡片内增加自动化区段,最多列出两个任务(图标、已存储的标题、频率与下次运行文本),还有更多任务被隐藏时显示遗漏行。处于更高优先级状态的行——有待处理交互、活动仍在进行,或有未查看的完成提醒——保留自身状态点且不渲染标记,两者不会同时出现。该标记和悬停区段读取共享的活动与已结束 Host 任务目录,并从中筛选本会话的活动任务;它们不会激活、保留或取消归档任何会话,也不读取 Session 日志。
|
||||
|
||||
-----
|
||||
|
||||
@@ -65,7 +65,7 @@ kind: "package-reference"
|
||||
<details>
|
||||
<summary>实现内部细节 — 点击展开</summary>
|
||||
|
||||
浏览器入口注册 `schedules` 主面板及对应的侧栏入口、Session 页头右侧功能位中位于 `order: -5` 且紧邻更多操作菜单左侧的 `schedule-catalog` 入口、经由 `ctx.uiConversation.events` 与 ui-chat 的 list seat `conversation.chat.turnTail` 在轮次层级注册的 `schedule_create` transcript 卡片(因此它渲染在该轮收尾的 assistant 文本之后,并在工具调用组折叠时仍然可见)、root 作用域的两个 Session 行 seat `sidebar.session.row.leading` 与 `sidebar.session.row.hover`,以及 kind 为 `scheduleTask` 的右侧栏页类型 `@deepseek-ai/dsh-client-ui-schedule/task`。该类型分两阶段注册:先把其定义注册进 `ctx.sidebarRightTabs`,再按该定义的 id 把正文与 chip 注册进带键 seat `sidebar.right.pane.tab` 与 `sidebar.right.pane.tab.title`。从 Session 入口打开任务时调用 `ctx.sidebarRight.openTab`,传入该入口的 Session id 与所选任务 id,详情因此出现在该入口所属 Session 的右侧栏中;自动化任务页面保留自己的列表并共用同一个详情组件。全局页面、任务页签、行标记与行悬停区段都从同一个 `schedule.catalog` 源读取活动与未运行任务;页头入口保留自己的按 Session `schedule.list` 读取,只返回该 Session 的活动任务。每个面都使用同一套可观察的查询生命周期,并在 `schedule/changed` 或连接重置后刷新。transcript 卡片从该调用持久化的结果 JSON 收窄出创建的任务,而不是依赖 Host 追加字段;该调用自身的工具调用组单元格由 ui-tool 的通用键控视图渲染,且卡片只对产生完整任务的调用出现。该对齐是因果判定而非时钟比较:目录快照带有 `readRequest`(最新被请求的读取序号)与 `readSettled`(产出当前记录的读取序号);Turn 尾部挂载时锚定 `readRequest` 并请求一次读取,仅在 `readSettled` 大于该锚点后才用其保留记录判定,且源把一次提交内的请求合并为单次 `list()`,只把该读取共享给尚未观察到它的调用者。比较浏览器时钟与 Host 时钟无法确立这一顺序。共用的删除回调与注入的 `onUpdateTiming` 保留所选记录的原 Session 绑定,而不使用当前打开的对话。详情保留当前任务的本地草稿、用于比较的权威值以及等待中或失败的保存;保存等待中或失败期间以及删除确认后,属主保留所显示的任务,使目录刷新在丢弃该行时既不能关闭已呈现的失败,也不能关闭陈述删除的详情。保存发出一次携带完整预期 `ScheduleRecord` 和分阶段变更的 `schedule.update` 请求;`onUpdateTiming` 刷新目录,而不把回读失败变成修改失败。
|
||||
浏览器入口注册 `schedules` 主面板及对应的侧栏入口、Session 页头右侧功能位中位于 `order: -5` 且紧邻更多操作菜单左侧的 `schedule-catalog` 入口、经由 `ctx.uiConversation.events` 与 ui-chat 的 list seat `conversation.chat.turnTail` 在轮次层级注册的 `schedule_create` transcript 卡片(因此它渲染在该轮收尾的 assistant 文本之后,并在工具调用组折叠时仍然可见)、root 作用域的两个 Session 行 seat `sidebar.session.row.leading` 与 `sidebar.session.row.hover`,以及 kind 为 `scheduleTask` 的右侧栏页类型 `@deepseek-ai/dsh-client-ui-schedule/task`。该类型分两阶段注册:先把其定义注册进 `ctx.sidebarRightTabs`,再按该定义的 id 把正文与 chip 注册进带键 seat `sidebar.right.pane.tab` 与 `sidebar.right.pane.tab.title`。从 Session 入口打开任务时调用 `ctx.sidebarRight.openTab`,传入该入口的 Session id 与所选任务 id,详情因此出现在该入口所属 Session 的右侧栏中;自动化任务页面保留自己的列表并共用同一个详情组件。全局页面、任务页签、行标记与行悬停区段都从同一个 `schedule.catalog` 源读取活动与已结束任务;页头入口保留自己的按 Session `schedule.list` 读取,只返回该 Session 的活动任务。每个面都使用同一套可观察的查询生命周期,并在 `schedule/changed` 或连接重置后刷新。transcript 卡片从该调用持久化的结果 JSON 收窄出创建的任务,而不是依赖 Host 追加字段;该调用自身的工具调用组单元格由 ui-tool 的通用键控视图渲染,且卡片只对产生完整任务的调用出现。该对齐是因果判定而非时钟比较:目录快照带有 `readRequest`(最新被请求的读取序号)与 `readSettled`(产出当前记录的读取序号);Turn 尾部挂载时锚定 `readRequest` 并请求一次读取,仅在 `readSettled` 大于该锚点后才用其保留记录判定,且源把一次提交内的请求合并为单次 `list()`,只把该读取共享给尚未观察到它的调用者。比较浏览器时钟与 Host 时钟无法确立这一顺序。共用的删除回调与注入的 `onUpdateTiming` 保留所选记录的原 Session 绑定,而不使用当前打开的对话。详情保留当前任务的本地草稿、用于比较的权威值以及等待中或失败的保存;保存等待中或失败期间以及删除确认后,属主保留所显示的任务,使目录刷新在丢弃该行时既不能关闭已呈现的失败,也不能关闭陈述删除的详情。保存发出一次携带完整预期 `ScheduleRecord` 和分阶段变更的 `schedule.update` 请求;`onUpdateTiming` 刷新目录,而不把回读失败变成修改失败。
|
||||
|
||||
刷新恢复出的任务标签没有导航参数,因为 Sidebar 只持久化标签的布局记录,不持久化打开方传入的内容。因此该页面类型自行保存从 Session 加布局记录 id 到该记录最后显示任务的 Session 和 id 的关联:每个 Session 一个 `dsh.schedule.task-tab.v1.<sessionId>` localStorage 键,按 tab id 一条记录,且从不保存任务内容。正文和 chip 只对没有导航参数的记录读取该条目,在标签出现之后成功的一次读取无法解析时将其删除,且不代替关联声称任务缺失。这样的一次读取无法为该标签给出任务标识时,正文陈述该状态,而不呈现永不结束的加载。
|
||||
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
/**
|
||||
* Transient outcome notice for schedule deletions.
|
||||
*
|
||||
* Every deletion — the Tasks page detail, the session task tab, and the
|
||||
* Session-header popover — reports its settled outcome into one store, and one
|
||||
* `shell.overlay` entry renders it as the app-wide banner. The surfaces keep no
|
||||
* in-place deletion notice of their own, so an outcome outlives the panel or
|
||||
* tab that asked for it.
|
||||
* Every deletion — the Tasks page detail, the session task tab, the
|
||||
* Session-header popover, and the Tasks page's removal of every ended task —
|
||||
* reports its settled outcome into one store, and one `shell.overlay` entry
|
||||
* renders it as the app-wide banner. The surfaces keep no in-place deletion
|
||||
* notice of their own, so an outcome outlives the panel or tab that asked for
|
||||
* it.
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import { IconWarningOutlineRegular, Toast } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import { assertNever } from '@deepseek-ai/dsh-util-values'
|
||||
import type { CatalogDeleteOutcome } from './catalog-source.ts'
|
||||
import type { ClearEndedOutcome } from './clear-ended.ts'
|
||||
|
||||
/** One reported deletion outcome; `seq` keys the banner so a re-show restarts it. */
|
||||
export interface DeleteToastState {
|
||||
readonly kind: 'deleted' | 'deleteFailed'
|
||||
readonly seq: number
|
||||
}
|
||||
/** One reported outcome; `seq` keys the banner so a re-show restarts it. */
|
||||
export type DeleteToastState =
|
||||
| { readonly kind: 'deleted' | 'deleteFailed'; readonly seq: number }
|
||||
| { readonly kind: 'cleared'; readonly count: number; readonly seq: number }
|
||||
| { readonly kind: 'clearFailed'; readonly seq: number }
|
||||
|
||||
/** Store the deletion wiring reports into and the overlay entry reads. */
|
||||
export interface DeleteToastSource {
|
||||
@@ -29,6 +32,11 @@ export interface DeleteToastSource {
|
||||
* @param outcome - resolution of `CatalogInjected.onDelete`.
|
||||
*/
|
||||
readonly report: (outcome: CatalogDeleteOutcome) => void
|
||||
/**
|
||||
* Show the notice of one settled bulk removal.
|
||||
* @param outcome - resolution of the management page's bulk removal callback.
|
||||
*/
|
||||
readonly reportClear: (outcome: ClearEndedOutcome) => void
|
||||
/** Clear the shown notice. */
|
||||
readonly dismiss: () => void
|
||||
}
|
||||
@@ -59,6 +67,11 @@ export function createDeleteToastSource(): DeleteToastSource {
|
||||
if (outcome === 'pending') return
|
||||
publish({ kind: outcome === 'deleted' ? 'deleted' : 'deleteFailed', seq: ++seq })
|
||||
},
|
||||
reportClear: (outcome) => {
|
||||
publish(outcome.cleared
|
||||
? { kind: 'cleared', count: outcome.count, seq: ++seq }
|
||||
: { kind: 'clearFailed', seq: ++seq })
|
||||
},
|
||||
dismiss: () => { publish(null) },
|
||||
}
|
||||
}
|
||||
@@ -66,23 +79,40 @@ export function createDeleteToastSource(): DeleteToastSource {
|
||||
/** Props of the `shell.overlay` entry: the notice store and localized copy. */
|
||||
export type ScheduleDeleteToastProps = PropsRuntime<'shell.overlay'>
|
||||
& PropsLocale<'schedule.manager'>
|
||||
& InjectFace<Omit<DeleteToastSource, 'report'>>
|
||||
& InjectFace<Omit<DeleteToastSource, 'report' | 'reportClear'>>
|
||||
|
||||
/**
|
||||
* Render the current deletion notice: a success banner for a confirmed
|
||||
* deletion, a warning for one that could not be confirmed, or nothing.
|
||||
* Render the current deletion notice: a success banner for a confirmed deletion
|
||||
* or settled bulk removal, a warning for one that could not be confirmed, or
|
||||
* nothing.
|
||||
* @param props - the notice hook, its dismissal, and the locale seat.
|
||||
* @returns the banner on display, or null.
|
||||
*/
|
||||
export function ScheduleDeleteToast({ useToast, dismiss, t }: ScheduleDeleteToastProps): ReactNode {
|
||||
const toast = useToast(current => current)
|
||||
if (toast === null) return null
|
||||
return toast.kind === 'deleted'
|
||||
? <Toast key={`schedule-delete-${String(toast.seq)}`} text={t('toast.deleted')} tone="success" onDone={dismiss} />
|
||||
: <Toast
|
||||
key={`schedule-delete-${String(toast.seq)}`}
|
||||
text={t('toast.deleteFailed')}
|
||||
icon={<IconWarningOutlineRegular />}
|
||||
onDone={dismiss}
|
||||
/>
|
||||
const key = `schedule-delete-${String(toast.seq)}`
|
||||
switch (toast.kind) {
|
||||
case 'deleted':
|
||||
return <Toast key={key} text={t('toast.deleted')} tone="success" onDone={dismiss} />
|
||||
case 'deleteFailed':
|
||||
return <Toast key={key} text={t('toast.deleteFailed')} icon={<IconWarningOutlineRegular />} onDone={dismiss} />
|
||||
case 'cleared':
|
||||
// A removal that found nothing states that instead of a zero count, and
|
||||
// without the success tone: the ended rows can leave between the
|
||||
// confirmation and the request.
|
||||
return toast.count === 0
|
||||
? <Toast key={key} text={t('toast.clearedNone')} onDone={dismiss} />
|
||||
: <Toast
|
||||
key={key}
|
||||
text={t(toast.count === 1 ? 'toast.cleared.one' : 'toast.cleared.other', { count: toast.count })}
|
||||
tone="success"
|
||||
onDone={dismiss}
|
||||
/>
|
||||
case 'clearFailed':
|
||||
return <Toast key={key} text={t('toast.clearFailed')} icon={<IconWarningOutlineRegular />} onDone={dismiss} />
|
||||
/* v8 ignore next -- the store publishes only the union's members, so the default holds no reachable statement. */
|
||||
default:
|
||||
return assertNever(toast)
|
||||
}
|
||||
}
|
||||
@@ -87,8 +87,9 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The Plugins toolbar's Add-plugin step: a 32px capsule with the 13px label. */
|
||||
.newButton {
|
||||
/* The Plugins toolbar's steps: New and the bulk cleanup share one 32px
|
||||
capsule with a 13px label. */
|
||||
.headerAction {
|
||||
height: 32px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
@@ -148,11 +149,36 @@
|
||||
neutral hairline, the 10px radius, and the mock's `--muted` leading icon. The
|
||||
mock brightens the border on hover and takes the brand stroke on focus
|
||||
rather than drawing a ring around the control. */
|
||||
.searchField {
|
||||
.searchTools {
|
||||
--task-search-height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin: 0 0 16px;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.catalogActions {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.catalogMore {
|
||||
width: var(--task-search-height);
|
||||
height: var(--task-search-height);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.catalogMore[aria-expanded='true'] {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.searchField {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
height: var(--task-search-height);
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
border: 0.5px solid var(--dsw-alias-border-l3);
|
||||
border-radius: 12px;
|
||||
@@ -1465,6 +1491,9 @@
|
||||
|
||||
.deleteButton {
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
/* The outlined confirmation button keeps its border in the danger colour so
|
||||
the destructive action reads as a bordered control, not bare text. */
|
||||
--dsh-button-outline-color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
.page :focus-visible {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
Button, IconClockOutlineRegular, IconCloseOutlineRegular, IconPlusOutlineRegular, IconSearchOutlineRegular, Input,
|
||||
Button, IconClockOutlineRegular, IconCloseOutlineRegular, IconEllipsisOutlineRegular, IconPlusOutlineRegular,
|
||||
IconSearchOutlineRegular, IconTrashOutlineRegular, Input, Menu, Modal, Tooltip,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
@@ -25,6 +26,16 @@ export interface TaskManagerInjected extends TaskDetailInjected {
|
||||
* schedule it. The page deliberately has no creation form of its own.
|
||||
*/
|
||||
readonly onNewTask: () => void
|
||||
/**
|
||||
* Remove every ended Host task, after this page has confirmed the action.
|
||||
*
|
||||
* The call requests no task ids, so the Host's ended set is the one removed;
|
||||
* the page's rows are only a snapshot that a delivery can have moved. It
|
||||
* resolves once the settled outcome has been reported through the app-wide
|
||||
* notice, which is the only place the result is stated.
|
||||
* @returns resolution after the outcome is reported.
|
||||
*/
|
||||
readonly onClearEnded: () => Promise<void>
|
||||
}
|
||||
|
||||
/** Root-scoped task catalog props derived from the framework and injected actions. */
|
||||
@@ -40,12 +51,15 @@ type StatusFilter = 'all' | ScheduleCatalogEntry['status']
|
||||
* @returns the searchable task list beside the selected task's detail.
|
||||
*/
|
||||
export function TaskManagerPage(props: TaskManagerPageProps) {
|
||||
const { useCatalog, onNewTask, onRetry, t } = props
|
||||
const { useCatalog, onNewTask, onClearEnded, onRetry, t } = props
|
||||
const catalog = useCatalog(snapshot => snapshot)
|
||||
const { records, status } = catalog
|
||||
const [search, setSearch] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all')
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('active')
|
||||
const [selectedId, setSelectedId] = useState<ScheduleId | null>(null)
|
||||
const [clearConfirm, setClearConfirm] = useState(false)
|
||||
const [clearMenu, setClearMenu] = useState(false)
|
||||
const [clearing, setClearing] = useState(false)
|
||||
// The rows state how long remains, so they read the shared ticking clock
|
||||
// rather than a value sampled at mount: a catalog refresh can move the target
|
||||
// this text describes.
|
||||
@@ -54,7 +68,20 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
|
||||
const { record: catalogRecord, task: selected, id: detailId, confirmId, setConfirmId, setTab } = detail
|
||||
const rowRef = useRef<HTMLButtonElement | null>(null)
|
||||
const headingRef = useRef<HTMLHeadingElement>(null)
|
||||
const clearTriggerRef = useRef<HTMLButtonElement | null>(null)
|
||||
// Set when the confirmation opens: its settled removal can hide the trigger
|
||||
// before the Modal returns focus, and so can another client's cleanup.
|
||||
const clearFocusPending = useRef(false)
|
||||
// Set when the removal settles: a readback superseded by `schedule/changed`
|
||||
// can publish its rows after the dialog closed, removing the trigger then.
|
||||
const clearSettled = useRef(false)
|
||||
const confirming = records.find(record => record.id === confirmId)
|
||||
// The bulk action's target and count come from the complete catalog, not from
|
||||
// the filtered rows: it removes every ended task, whatever the view shows.
|
||||
const endedCount = useMemo(
|
||||
() => records.filter(record => record.status === 'inactive').length,
|
||||
[records],
|
||||
)
|
||||
const rows = useMemo(() => {
|
||||
const query = search.trim().toLowerCase()
|
||||
return records.filter(record => (
|
||||
@@ -79,13 +106,15 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
|
||||
useEffect(() => {
|
||||
// The detail focuses its own panel; with nothing selected, focus returns
|
||||
// to the row that opened it, or to the page heading once that row is gone.
|
||||
if (selectedId !== null) return
|
||||
// An open confirmation owns the focus until it closes, even when a refresh
|
||||
// drops the selected task behind it.
|
||||
if (selectedId !== null || clearConfirm) return
|
||||
if (rowRef.current !== null) {
|
||||
const target = rowRef.current.isConnected ? rowRef.current : headingRef.current
|
||||
target?.focus()
|
||||
rowRef.current = null
|
||||
}
|
||||
}, [selectedId])
|
||||
}, [selectedId, clearConfirm])
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'ready') return
|
||||
@@ -97,16 +126,55 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
|
||||
setSelectedId(null)
|
||||
}
|
||||
|
||||
// The Modal restores focus to the element it captured when it opened; the
|
||||
// trigger can be removed or rebuilt while the confirmation is open, so the
|
||||
// page points focus at the current control, or the heading when it is gone.
|
||||
// A settled removal whose readback lands later unmounts the trigger after the
|
||||
// dialog closed, which drops focus to the document body; that handoff is
|
||||
// caught by the same effect watching the ended count.
|
||||
useEffect(() => {
|
||||
if (clearConfirm) return
|
||||
if (clearFocusPending.current) {
|
||||
clearFocusPending.current = false
|
||||
const trigger = clearTriggerRef.current
|
||||
if (trigger?.isConnected === true) {
|
||||
trigger.focus()
|
||||
return
|
||||
}
|
||||
clearSettled.current = false
|
||||
headingRef.current?.focus()
|
||||
return
|
||||
}
|
||||
if (!clearSettled.current || endedCount > 0 || document.activeElement !== document.body) return
|
||||
clearSettled.current = false
|
||||
headingRef.current?.focus()
|
||||
}, [clearConfirm, endedCount])
|
||||
|
||||
const confirmClearEnded = (): void => {
|
||||
// The confirmation button stays focusable while the removal runs, so a
|
||||
// second press has to be ignored here instead of by `disabled`.
|
||||
if (clearing) return
|
||||
setClearing(true)
|
||||
void onClearEnded().finally(() => {
|
||||
clearSettled.current = true
|
||||
setClearing(false)
|
||||
setClearConfirm(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className={clsx(css.page, selected !== undefined && css.hasDetails)}
|
||||
aria-label={t('title')}
|
||||
data-testid="task-manager-page"
|
||||
onKeyDown={(event) => {
|
||||
// A dropdown that closed on this Escape has already consumed the key: the
|
||||
// Menu primitive closes from the document capture phase and calls
|
||||
// preventDefault there, before React dispatches this handler.
|
||||
if (event.key !== 'Escape' || event.defaultPrevented || confirmId !== null || selectedId === null) return
|
||||
// An Escape that closed a dropdown is already consumed by the Menu's
|
||||
// document capture listener. A confirmation's own Escape reaches the
|
||||
// Modal's document bubble listener, which runs after this React handler,
|
||||
// so the `clearConfirm` guard is what keeps this page from also closing
|
||||
// the detail while the dialog closes.
|
||||
if (event.key !== 'Escape' || event.defaultPrevented || confirmId !== null || clearConfirm
|
||||
|| selectedId === null) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
closeDetails()
|
||||
@@ -118,12 +186,12 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
|
||||
<div className={css.pageHeading}>
|
||||
<h1 ref={headingRef} tabIndex={-1}>{t('title')}</h1>
|
||||
<div className={css.creationActions}>
|
||||
<Button variant="primary" size="sm" className={css.newButton} icon={<IconPlusOutlineRegular size={13} />} onClick={onNewTask}>{t('new.action')}</Button>
|
||||
<Button variant="primary" size="sm" className={css.headerAction} icon={<IconPlusOutlineRegular size={13} />} onClick={onNewTask}>{t('new.action')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.filters}>
|
||||
<div className={css.filterTabs} role="group" aria-label={t('statusFilter.label')}>
|
||||
{(['all', 'active', 'inactive'] as const).map(value => (
|
||||
{(['active', 'inactive', 'all'] as const).map(value => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
@@ -136,18 +204,50 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.searchField}>
|
||||
<Input
|
||||
type="search"
|
||||
icon={<IconSearchOutlineRegular />}
|
||||
aria-label={t('search.label')}
|
||||
placeholder={t('search.placeholder')}
|
||||
value={search}
|
||||
onChange={(event) => { setSearch(event.target.value) }}
|
||||
/>
|
||||
{search !== '' && <Button size="sm" className={css.searchClear} aria-label={t('search.clear')} onClick={() => { setSearch('') }}>
|
||||
<IconCloseOutlineRegular />
|
||||
</Button>}
|
||||
<div className={css.searchTools}>
|
||||
<div className={css.searchField}>
|
||||
<Input
|
||||
type="search"
|
||||
icon={<IconSearchOutlineRegular />}
|
||||
aria-label={t('search.label')}
|
||||
placeholder={t('search.placeholder')}
|
||||
value={search}
|
||||
onChange={(event) => { setSearch(event.target.value) }}
|
||||
/>
|
||||
{search !== '' && <Button size="sm" className={css.searchClear} aria-label={t('search.clear')} onClick={() => { setSearch('') }}>
|
||||
<IconCloseOutlineRegular />
|
||||
</Button>}
|
||||
</div>
|
||||
{statusFilter !== 'active' && endedCount > 0 && <Menu
|
||||
open={clearMenu}
|
||||
onClose={() => { setClearMenu(false) }}
|
||||
align="end"
|
||||
portal
|
||||
items={[{
|
||||
id: 'clear-inactive', label: t(clearing ? 'clear.pending' : 'clear.action'),
|
||||
icon: <IconTrashOutlineRegular />, danger: true,
|
||||
disabled: clearing || status !== 'ready',
|
||||
}]}
|
||||
onSelect={() => {
|
||||
setClearMenu(false)
|
||||
clearFocusPending.current = true
|
||||
setClearConfirm(true)
|
||||
}}
|
||||
anchor={<Tooltip label={t('list.more')} side="bottom" portal disabled={clearMenu}>
|
||||
<Button
|
||||
ref={clearTriggerRef}
|
||||
variant="outline"
|
||||
className={css.catalogMore}
|
||||
aria-label={t('list.more')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={clearMenu}
|
||||
aria-busy={clearing}
|
||||
onClick={() => { setClearMenu(value => !value) }}
|
||||
>
|
||||
<IconEllipsisOutlineRegular />
|
||||
</Button>
|
||||
</Tooltip>}
|
||||
/>}
|
||||
</div>
|
||||
<div className={css.list}>
|
||||
{selected === undefined && <CatalogFeedback status={status} populated={rows.length > 0} onRetry={onRetry} t={t} />}
|
||||
@@ -209,6 +309,31 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
|
||||
onDeleted={closeDetails}
|
||||
onClose={closeDetails}
|
||||
/>}
|
||||
<Modal
|
||||
className={css.confirmDialog as string}
|
||||
contentClassName={css.confirmContent as string}
|
||||
open={clearConfirm}
|
||||
title={t('clear.title', { count: endedCount })}
|
||||
description={t('clear.description')}
|
||||
closeLabel={t('clear.close')}
|
||||
// Dismissal waits for the settled removal: the confirmation is the only
|
||||
// place that states the operation is still running.
|
||||
onClose={() => { if (!clearing) setClearConfirm(false) }}
|
||||
footer={<div className={css.confirmActions}>
|
||||
<Button variant="outline" disabled={clearing} onClick={() => { setClearConfirm(false) }}>
|
||||
{t('delete.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={css.deleteButton}
|
||||
aria-disabled={clearing}
|
||||
onClick={confirmClearEnded}
|
||||
>
|
||||
{t(clearing ? 'clear.pending' : 'clear.confirm')}
|
||||
</Button>
|
||||
</div>}
|
||||
/>
|
||||
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/** Host-wide removal of every ended task, as the management page's one action reports it. */
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ScheduleDeleteInactiveResult } from '@deepseek-ai/dsh-schedule/client'
|
||||
|
||||
/** How one bulk removal settled; the wiring reports it as an app-wide notice. */
|
||||
export type ClearEndedOutcome =
|
||||
| { readonly cleared: true; readonly count: number }
|
||||
| { readonly cleared: false }
|
||||
|
||||
/** Remote removal and the authoritative readback that follows it. */
|
||||
export interface ClearEndedDependencies {
|
||||
/** Ask the Host to remove every ended task; it answers how many rows it removed. */
|
||||
readonly removeInactive: () => Promise<RemoteResult<ScheduleDeleteInactiveResult>>
|
||||
/**
|
||||
* Read the catalog again, naming the newest request ordinal so a read sent
|
||||
* before the removal is superseded rather than reused.
|
||||
* @returns resolution after the read result is published.
|
||||
*/
|
||||
readonly refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the management page's bulk removal callback.
|
||||
*
|
||||
* The call names no task ids: the Host owns which rows are ended, and a
|
||||
* delivery can have moved the catalog between the confirmation and the
|
||||
* request. The readback follows the acknowledged removal and supersedes a read
|
||||
* issued before it, so the list follows the removal rather than a stale read.
|
||||
* @param deps - Remote removal and its catalog readback.
|
||||
* @returns a callback that resolves once the settled outcome is available.
|
||||
*/
|
||||
export function createClearEnded(deps: ClearEndedDependencies): () => Promise<ClearEndedOutcome> {
|
||||
return async () => {
|
||||
let result: RemoteResult<ScheduleDeleteInactiveResult>
|
||||
try {
|
||||
result = await deps.removeInactive()
|
||||
} catch (_error: unknown) {
|
||||
// A rejected request does not establish which rows changed.
|
||||
return { cleared: false }
|
||||
}
|
||||
if (!result.ok) return { cleared: false }
|
||||
await deps.refresh()
|
||||
return { cleared: true, count: result.value.deleted }
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import type {} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { ScheduleCatalogEntry, ScheduleDeliveryHistoryRequest, ScheduleId, ScheduleUpdateRequest } from '@deepseek-ai/dsh-schedule/client'
|
||||
import { createCatalogSource, type CatalogDeleteOutcome, type CatalogInjected } from './catalog-source.ts'
|
||||
import { createClearEnded } from './clear-ended.ts'
|
||||
import { createDeleteToastSource, ScheduleDeleteToast } from './DeleteToast.tsx'
|
||||
import { SCHEDULE_TASK_ID, SCHEDULE_TASK_KIND, scheduleTaskDefinition } from './definition.ts'
|
||||
import { ScheduleCatalogAction } from './ScheduleCatalogAction.tsx'
|
||||
@@ -145,6 +146,17 @@ export function apply(ctx: ClientContext): void {
|
||||
ctx.uiWorkspace.openSession(id)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Remove every ended Host task and read the catalog back.
|
||||
*
|
||||
* The readback names every request seen so far, so it supersedes a read sent
|
||||
* before the removal instead of joining it.
|
||||
* @returns the settled bulk-removal outcome.
|
||||
*/
|
||||
const clearEnded = createClearEnded({
|
||||
removeInactive: () => ctx.remote.schedule.deleteInactive(),
|
||||
refresh: () => manager.onRetry(manager.hooks.catalog.getSnapshot().readRequest),
|
||||
})
|
||||
const detail: TaskDetailInjected & ScheduleTaskCatalogInjected = {
|
||||
hooks: manager.hooks,
|
||||
onDelete: reportedDelete(manager.onDelete),
|
||||
@@ -180,6 +192,10 @@ export function apply(ctx: ClientContext): void {
|
||||
...detail,
|
||||
// The page has no creation form: a new reminder starts in a Session.
|
||||
onNewTask: () => { ctx.uiWorkspace.startSession() },
|
||||
onClearEnded: async () => {
|
||||
const outcome = await clearEnded()
|
||||
deleteToast.reportClear(outcome)
|
||||
},
|
||||
}),
|
||||
}, TaskManagerPage))
|
||||
yield ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
|
||||
|
||||
@@ -134,8 +134,20 @@ export const en = {
|
||||
'delete.cancel': 'Cancel',
|
||||
'delete.close': 'Close deletion confirmation',
|
||||
'delete.pending': 'Deleting…',
|
||||
'list.more': 'More task actions',
|
||||
'clear.action': 'Clear all inactive tasks',
|
||||
'clear.title': 'Clear {count} inactive tasks?',
|
||||
'clear.description': 'These tasks and their saved delivery records will be permanently deleted. '
|
||||
+ 'Enabled tasks, original sessions, and queued messages are not affected.',
|
||||
'clear.confirm': 'Clear tasks',
|
||||
'clear.pending': 'Clearing…',
|
||||
'clear.close': 'Close clear confirmation',
|
||||
'toast.deleted': 'Task deleted.',
|
||||
'toast.deleteFailed': 'Could not delete the task.',
|
||||
'toast.cleared.one': 'Cleared {count} inactive task.',
|
||||
'toast.cleared.other': 'Cleared {count} inactive tasks.',
|
||||
'toast.clearedNone': 'No inactive tasks to clear.',
|
||||
'toast.clearFailed': 'Could not clear tasks.',
|
||||
'card.open': 'Open',
|
||||
'card.openLabel': 'Open task details: {title}',
|
||||
'card.deleted': 'Deleted',
|
||||
@@ -276,8 +288,19 @@ export const zh: Record<TaskManagerKey, string> = {
|
||||
'delete.cancel': '取消',
|
||||
'delete.close': '关闭删除确认',
|
||||
'delete.pending': '正在删除…',
|
||||
'list.more': '更多任务操作',
|
||||
'clear.action': '清除全部已结束任务',
|
||||
'clear.title': '清除 {count} 个已结束任务?',
|
||||
'clear.description': '这些任务及其运行记录将被永久删除,已开启任务、原会话和已排队消息等不受影响。',
|
||||
'clear.confirm': '确认清除',
|
||||
'clear.pending': '正在清理…',
|
||||
'clear.close': '关闭清理确认',
|
||||
'toast.deleted': '任务已删除',
|
||||
'toast.deleteFailed': '无法删除任务',
|
||||
'toast.cleared.one': '已清除 {count} 个已结束任务',
|
||||
'toast.cleared.other': '已清除 {count} 个已结束任务',
|
||||
'toast.clearedNone': '没有可清理的已结束任务',
|
||||
'toast.clearFailed': '清除任务失败',
|
||||
'card.open': '打开',
|
||||
'card.openLabel': '打开任务详情:{title}',
|
||||
'card.deleted': '已删除',
|
||||
@@ -415,8 +438,19 @@ export const ja = {
|
||||
'delete.cancel': 'キャンセル',
|
||||
'delete.close': '削除確認を閉じる',
|
||||
'delete.pending': '削除中…',
|
||||
'list.more': 'その他のタスク操作',
|
||||
'clear.action': '終了済みタスクをすべてクリア',
|
||||
'clear.title': '終了済みタスク {count} 件をクリアしますか?',
|
||||
'clear.description': 'これらのタスクと実行履歴は完全に削除されます。実行中のタスク、元のセッション、キューに入ったメッセージなどは影響を受けません。',
|
||||
'clear.confirm': 'クリアする',
|
||||
'clear.pending': 'クリア中…',
|
||||
'clear.close': 'クリア確認を閉じる',
|
||||
'toast.deleted': 'タスクを削除しました。',
|
||||
'toast.deleteFailed': 'タスクを削除できませんでした。',
|
||||
'toast.cleared.one': '終了したタスクを {count} 件クリアしました。',
|
||||
'toast.cleared.other': '終了したタスクを {count} 件クリアしました。',
|
||||
'toast.clearedNone': 'クリアする終了済みタスクはありません。',
|
||||
'toast.clearFailed': 'タスクをクリアできませんでした。',
|
||||
'card.open': '開く',
|
||||
'card.openLabel': 'タスクの詳細を開く: {title}',
|
||||
'card.deleted': '削除済み',
|
||||
|
||||
@@ -11,7 +11,9 @@ import { apply as applyNode } from '../src/index.ts'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
import type { ScheduleId, ScheduleCatalogEntry, ScheduleUpdateRequest, ScheduleUpdateResult } from '@deepseek-ai/dsh-schedule/client'
|
||||
import type {
|
||||
ScheduleId, ScheduleCatalogEntry, ScheduleDeleteInactiveResult, ScheduleUpdateRequest, ScheduleUpdateResult,
|
||||
} from '@deepseek-ai/dsh-schedule/client'
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { CatalogInjected } from '../src/client/catalog-source.ts'
|
||||
@@ -496,6 +498,83 @@ describe('ui-schedule browser half', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('clears every ended task from the global page, reads the catalog back, and reports the count', async () => {
|
||||
const record: ScheduleCatalogEntry = {
|
||||
id: 'host-ended' as ScheduleId, sessionId: 'cold-original' as SessionId, status: 'inactive',
|
||||
kind: 'after', title: 'Ended review', prompt: 'Ended review', afterSeconds: 60,
|
||||
scheduledAt: '2099-01-01T00:00:00.000Z',
|
||||
}
|
||||
const catalog = vi.fn()
|
||||
.mockResolvedValueOnce({ ok: true, value: [record] })
|
||||
.mockResolvedValue({ ok: true, value: [] })
|
||||
const deleteInactive = vi.fn().mockResolvedValue({ ok: true, value: { deleted: 1 } })
|
||||
const ctx = await baseContext({ schedule: { catalog, deleteInactive }, $on: () => () => {} })
|
||||
const owner = ctx.slots.register({
|
||||
name: 'root',
|
||||
children: {
|
||||
main: { kind: 'keyed', scope: 'root' },
|
||||
'shell.overlay': { kind: 'list', scope: 'root' },
|
||||
},
|
||||
} as never, Empty)
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
try {
|
||||
const face = injectedFace(ctx.slots.entries('main')[0]!, undefined as never) as TaskManagerInjected
|
||||
const toast = injectedFace(ctx.slots.entries('shell.overlay')[0]!) as Omit<DeleteToastSource, 'report' | 'reportClear'>
|
||||
const dispose = face.hooks.catalog.subscribe(vi.fn())
|
||||
try {
|
||||
await vi.waitFor(() => { expect(face.hooks.catalog.getSnapshot().records).toEqual([record]) })
|
||||
const reads = catalog.mock.calls.length
|
||||
await face.onClearEnded()
|
||||
expect(deleteInactive).toHaveBeenCalledOnce()
|
||||
// The acknowledged removal reads the catalog back instead of joining a
|
||||
// read issued before it, so the page's rows follow the removal.
|
||||
expect(catalog.mock.calls.length).toBe(reads + 1)
|
||||
await vi.waitFor(() => { expect(face.hooks.catalog.getSnapshot().records).toEqual([]) })
|
||||
expect(toast.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 1, seq: 1 })
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
} finally {
|
||||
await fiber.dispose()
|
||||
owner()
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('reports a settled bulk removal after the page registration is gone', async () => {
|
||||
const catalog = vi.fn().mockResolvedValue({ ok: true, value: [] })
|
||||
const removal = Promise.withResolvers<{ ok: true; value: ScheduleDeleteInactiveResult }>()
|
||||
const deleteInactive = vi.fn(() => removal.promise)
|
||||
const ctx = await baseContext({ schedule: { catalog, deleteInactive }, $on: () => () => {} })
|
||||
const owner = ctx.slots.register({
|
||||
name: 'root',
|
||||
children: {
|
||||
main: { kind: 'keyed', scope: 'root' },
|
||||
'shell.overlay': { kind: 'list', scope: 'root' },
|
||||
},
|
||||
} as never, Empty)
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
let claimed = false
|
||||
try {
|
||||
const face = injectedFace(ctx.slots.entries('main')[0]!, undefined as never) as TaskManagerInjected
|
||||
const toast = injectedFace(ctx.slots.entries('shell.overlay')[0]!) as Omit<DeleteToastSource, 'report' | 'reportClear'>
|
||||
const clearing = face.onClearEnded()
|
||||
// The page panel can unmount while the Host removal is still in flight;
|
||||
// the notice store belongs to the plugin, so the outcome still publishes.
|
||||
owner()
|
||||
claimed = true
|
||||
removal.resolve({ ok: true, value: { deleted: 2 } })
|
||||
await clearing
|
||||
expect(toast.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 2, seq: 1 })
|
||||
} finally {
|
||||
if (!claimed) owner()
|
||||
await fiber.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('registers the row mark and hover-card seats under the Session row declarations', async () => {
|
||||
const catalog = vi.fn().mockResolvedValue({ ok: true, value: [] })
|
||||
const ctx = await baseContext({ schedule: { catalog }, $on: () => () => {} })
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/** The management page's bulk removal: one request, one readback, one settled outcome. */
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ScheduleDeleteInactiveResult } from '@deepseek-ai/dsh-schedule/client'
|
||||
import { createClearEnded } from '../src/client/clear-ended.ts'
|
||||
|
||||
const failure: RemoteResult<ScheduleDeleteInactiveResult> = {
|
||||
ok: false, error: new RemoteError('gateway/internal', 'Unavailable', {}),
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire the helper to one fake Remote answer and one readback spy.
|
||||
* @param answer - what the removal call answers, or the reason it rejects.
|
||||
* @param refresh - the readback the helper calls after an acknowledged removal.
|
||||
* @returns the callback under test with its dependencies' spies.
|
||||
*/
|
||||
function setup(
|
||||
answer: RemoteResult<ScheduleDeleteInactiveResult> | Error,
|
||||
refresh = vi.fn(async () => {}),
|
||||
) {
|
||||
const removeInactive = vi.fn(async () => {
|
||||
if (answer instanceof Error) throw answer
|
||||
return answer
|
||||
})
|
||||
return { clearEnded: createClearEnded({ removeInactive, refresh }), removeInactive, refresh }
|
||||
}
|
||||
|
||||
describe('createClearEnded', () => {
|
||||
it('reads the catalog back before the outcome settles and reports how many rows went', async () => {
|
||||
const readback = Promise.withResolvers<undefined>()
|
||||
const { clearEnded, removeInactive, refresh } = setup(
|
||||
{ ok: true, value: { deleted: 3 } },
|
||||
vi.fn(() => readback.promise),
|
||||
)
|
||||
let settled = false
|
||||
const outcome = clearEnded().then((value) => {
|
||||
settled = true
|
||||
return value
|
||||
})
|
||||
|
||||
await vi.waitFor(() => { expect(refresh).toHaveBeenCalledOnce() })
|
||||
// The list must agree with the count before the notice states it, so the
|
||||
// outcome waits for the readback rather than publishing alongside it.
|
||||
expect(settled).toBe(false)
|
||||
readback.resolve(undefined)
|
||||
|
||||
await expect(outcome).resolves.toEqual({ cleared: true, count: 3 })
|
||||
expect(removeInactive).toHaveBeenCalledOnce()
|
||||
expect(removeInactive.mock.invocationCallOrder[0])
|
||||
.toBeLessThan(refresh.mock.invocationCallOrder[0]!)
|
||||
})
|
||||
|
||||
it('reports an acknowledged removal that found nothing as a cleared outcome', async () => {
|
||||
const { clearEnded, refresh } = setup({ ok: true, value: { deleted: 0 } })
|
||||
|
||||
await expect(clearEnded()).resolves.toEqual({ cleared: true, count: 0 })
|
||||
expect(refresh).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('reports a refused removal and reads nothing back', async () => {
|
||||
const { clearEnded, refresh } = setup(failure)
|
||||
|
||||
await expect(clearEnded()).resolves.toEqual({ cleared: false })
|
||||
expect(refresh).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports a rejected removal and reads nothing back', async () => {
|
||||
const { clearEnded, refresh } = setup(new Error('connection lost'))
|
||||
|
||||
await expect(clearEnded()).resolves.toEqual({ cleared: false })
|
||||
expect(refresh).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -48,6 +48,17 @@ describe('delete toast source', () => {
|
||||
source.report('deleted')
|
||||
expect(listener).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('publishes each settled bulk removal with its count or failure', () => {
|
||||
const source = createDeleteToastSource()
|
||||
source.reportClear({ cleared: true, count: 3 })
|
||||
expect(source.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 3, seq: 1 })
|
||||
// A removal that found nothing keeps its own zero, so the banner can say so.
|
||||
source.reportClear({ cleared: true, count: 0 })
|
||||
expect(source.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 0, seq: 2 })
|
||||
source.reportClear({ cleared: false })
|
||||
expect(source.hooks.toast.getSnapshot()).toEqual({ kind: 'clearFailed', seq: 3 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('ScheduleDeleteToast', () => {
|
||||
@@ -68,4 +79,25 @@ describe('ScheduleDeleteToast', () => {
|
||||
mount({ kind: 'deleteFailed', seq: 2 }, zh)
|
||||
expect(screen.getByRole('alert').textContent).toBe(zh['toast.deleteFailed'])
|
||||
})
|
||||
|
||||
it('states how many inactive tasks a bulk removal cleared, singular for one', () => {
|
||||
vi.useFakeTimers()
|
||||
const many = mount({ kind: 'cleared', count: 4, seq: 3 })
|
||||
expect(screen.getByRole('alert').textContent).toBe(en['toast.cleared.other'].replace('{count}', '4'))
|
||||
act(() => { vi.runAllTimers() })
|
||||
expect(many.dismiss).toHaveBeenCalledOnce()
|
||||
cleanup()
|
||||
mount({ kind: 'cleared', count: 1, seq: 4 })
|
||||
expect(screen.getByRole('alert').textContent).toBe(en['toast.cleared.one'].replace('{count}', '1'))
|
||||
})
|
||||
|
||||
it('states that a bulk removal found nothing instead of a zero count', () => {
|
||||
mount({ kind: 'cleared', count: 0, seq: 4 }, zh)
|
||||
expect(screen.getByRole('alert').textContent).toBe(zh['toast.clearedNone'])
|
||||
})
|
||||
|
||||
it('shows the warning banner for a bulk removal that failed', () => {
|
||||
mount({ kind: 'clearFailed', seq: 5 }, zh)
|
||||
expect(screen.getByRole('alert').textContent).toBe(zh['toast.clearFailed'])
|
||||
})
|
||||
})
|
||||
@@ -122,6 +122,7 @@ function mount(
|
||||
onDelete: vi.fn<TaskManagerPageProps['onDelete']>(async () => 'deleted'),
|
||||
onRetry: vi.fn(async () => {}),
|
||||
onNewTask: vi.fn(),
|
||||
onClearEnded: vi.fn(async () => {}),
|
||||
onUpdateTiming: vi.fn<TaskManagerPageProps['onUpdateTiming']>(async ({ expected }) => ({
|
||||
ok: true, value: { id: expected.id, updated: false, record: expected },
|
||||
})),
|
||||
|
||||
@@ -162,6 +162,7 @@ function mount(
|
||||
initial: Partial<CatalogSnapshot<ScheduleCatalogEntry>> = {},
|
||||
dictionary: typeof en | typeof zh = en,
|
||||
overrides: Partial<TaskManagerPageProps> = {},
|
||||
options: { startAtAll?: boolean } = {},
|
||||
) {
|
||||
let snapshot: CatalogSnapshot<ScheduleCatalogEntry> = {
|
||||
records, status: 'ready', deleting: [], settled: true, readRequest: 0, readSettled: 0, ...initial,
|
||||
@@ -178,6 +179,7 @@ function mount(
|
||||
onDelete: vi.fn<TaskManagerPageProps['onDelete']>(async () => 'deleted'),
|
||||
onRetry: vi.fn(async () => {}),
|
||||
onNewTask: vi.fn(),
|
||||
onClearEnded: vi.fn(async () => {}),
|
||||
onUpdateTiming: vi.fn<TaskManagerPageProps['onUpdateTiming']>(async ({ expected }) => ({
|
||||
ok: true, value: { id: expected.id, updated: false, record: expected },
|
||||
})),
|
||||
@@ -193,6 +195,11 @@ function mount(
|
||||
...overrides,
|
||||
}
|
||||
const view = render(<TaskManagerPage {...props} />)
|
||||
// The page starts on Active; cases about the full list select All first. The
|
||||
// default filter itself is covered by its own case below.
|
||||
if (options.startAtAll !== false) {
|
||||
fireEvent.click(screen.getByRole('button', { name: dictionary['statusFilter.all'] }))
|
||||
}
|
||||
return {
|
||||
props, view,
|
||||
updateTiming: vi.mocked(props.onUpdateTiming),
|
||||
@@ -640,9 +647,11 @@ describe('Task manager catalog', () => {
|
||||
expect(within(filters).getAllByRole('group')).toHaveLength(1)
|
||||
const statuses = within(filters).getByRole('group', { name: en['statusFilter.label'] })
|
||||
expect(statuses.parentElement).toBe(filters)
|
||||
expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe(en['statusFilter.all'])
|
||||
expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe(en['status.active'])
|
||||
// The mock's `.searchbox`: the same field carries the leading glyph, the
|
||||
// placeholder, and — once a value exists — the inline clear control.
|
||||
const searchTools = page.querySelector<HTMLElement>(`.${css.searchTools!}`)!
|
||||
expect(searchTools.contains(page.querySelector(`.${css.searchField!}`))).toBe(true)
|
||||
const field = page.querySelector<HTMLElement>(`.${css.searchField!}`)!
|
||||
const input = screen.getByRole('searchbox', { name: en['search.label'] })
|
||||
expect(field.contains(input)).toBe(true)
|
||||
@@ -835,6 +844,18 @@ describe('Task manager catalog', () => {
|
||||
expect(taskNames()).toEqual(['Send summary', 'Review release', 'Check metrics'])
|
||||
})
|
||||
|
||||
it.each([en, zh])('starts on the active status filter', (dictionary) => {
|
||||
mount({ records: [ended, every, after] }, dictionary, {}, { startAtAll: false })
|
||||
const list = screen.getByRole('list', { name: dictionary['list.label'] })
|
||||
const names = within(list).queryAllByRole('button')
|
||||
.filter(button => button.closest('li') !== null)
|
||||
.map(button => button.getAttribute('aria-label'))
|
||||
const statuses = screen.getByRole('group', { name: dictionary['statusFilter.label'] })
|
||||
expect(within(statuses).getByRole('button', { name: dictionary['status.active'] }).getAttribute('aria-pressed')).toBe('true')
|
||||
expect(within(statuses).getByRole('button', { name: dictionary['statusFilter.all'] }).getAttribute('aria-pressed')).toBe('false')
|
||||
expect(names).toEqual(['Send summary', 'Check metrics'])
|
||||
})
|
||||
|
||||
it.each([en, zh])('combines all, active, and inactive status filters with search', (dictionary) => {
|
||||
mount({ records: [ended, every, after] }, dictionary)
|
||||
const list = screen.getByRole('list', { name: dictionary['list.label'] })
|
||||
@@ -942,6 +963,209 @@ describe('Task manager catalog', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearing inactive tasks', () => {
|
||||
// A distinct identity: the module's `ended` case reuses the active row's id.
|
||||
const endedTask: ScheduleCatalogEntry = {
|
||||
...ended, id: 'task-ended' as ScheduleId, title: 'Ended summary', prompt: 'Ended summary',
|
||||
}
|
||||
const anotherEnded: ScheduleCatalogEntry = {
|
||||
...ended, id: 'task-ended-two' as ScheduleId, title: 'Ended report', prompt: 'Ended report',
|
||||
}
|
||||
|
||||
/** The heading's actions; only the primary New control remains there. */
|
||||
function creationActions(): HTMLElement {
|
||||
return document.querySelector<HTMLElement>(`.${css.creationActions!}`)!
|
||||
}
|
||||
|
||||
/** The search row's 36px More trigger. */
|
||||
function moreTrigger(dictionary: typeof en | typeof zh = en): HTMLElement {
|
||||
return screen.getByRole('button', { name: dictionary['list.more'] })
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the cleanup confirmation through the More menu.
|
||||
* @param dictionary - locale under test.
|
||||
* @param count - ended rows the page's complete catalog holds.
|
||||
* @returns the confirmation dialog.
|
||||
*/
|
||||
function openClear(dictionary: typeof en | typeof zh = en, count = 1): HTMLElement {
|
||||
fireEvent.click(moreTrigger(dictionary))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: dictionary['clear.action'] }))
|
||||
return screen.getByRole('dialog', {
|
||||
name: dictionary['clear.title'].replace('{count}', String(count)),
|
||||
})
|
||||
}
|
||||
|
||||
it('offers the cleanup in the search row only while an ended task is visible', () => {
|
||||
const h = mount()
|
||||
expect(creationActions().textContent).toBe(en['new.action'])
|
||||
expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull()
|
||||
h.update({ records: [...records, endedTask] })
|
||||
// The cleanup is the search row's More menu, not a second heading action.
|
||||
expect(document.querySelector(`.${css.searchTools!}`)?.contains(moreTrigger())).toBe(true)
|
||||
expect(within(creationActions()).getAllByRole('button')).toHaveLength(1)
|
||||
// The Active view hides the cleanup even though the catalog holds ended rows.
|
||||
fireEvent.click(screen.getByRole('button', { name: en['status.active'] }))
|
||||
expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: en['statusFilter.all'] }))
|
||||
expect(moreTrigger()).toBeDefined()
|
||||
})
|
||||
|
||||
it('states the complete ended set in the confirmation, not the filtered rows', () => {
|
||||
mount({ records: [...records, endedTask, anotherEnded] })
|
||||
fireEvent.change(screen.getByRole('searchbox', { name: en['search.label'] }), {
|
||||
target: { value: 'Send summary' },
|
||||
})
|
||||
expect(taskNames()).toEqual(['Send summary'])
|
||||
const dialog = openClear(en, 2)
|
||||
expect(within(dialog).getByText(en['clear.description'])).toBeDefined()
|
||||
})
|
||||
|
||||
it('closes the cleanup menu without opening the confirmation', () => {
|
||||
mount({ records: [at, endedTask] })
|
||||
fireEvent.click(moreTrigger())
|
||||
expect(screen.getByRole('menuitem', { name: en['clear.action'] })).toBeDefined()
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(screen.queryByRole('menuitem', { name: en['clear.action'] })).toBeNull()
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it('removes nothing when the confirmation is dismissed', () => {
|
||||
const h = mount({ records: [at, endedTask] })
|
||||
openClear()
|
||||
fireEvent.click(screen.getByRole('button', { name: en['delete.cancel'] }))
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
expect(h.props.onClearEnded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the confirmation open until the settled removal, then returns focus', async () => {
|
||||
const pending = Promise.withResolvers<undefined>()
|
||||
const onClearEnded = vi.fn(() => pending.promise)
|
||||
mount({ records: [at, endedTask] }, en, { onClearEnded })
|
||||
const trigger = moreTrigger()
|
||||
trigger.focus()
|
||||
const dialog = openClear()
|
||||
const confirm = within(dialog).getByRole('button', { name: en['clear.confirm'] })
|
||||
confirm.focus()
|
||||
fireEvent.click(confirm)
|
||||
expect(onClearEnded).toHaveBeenCalledOnce()
|
||||
|
||||
const running = screen.getByRole('dialog')
|
||||
const runningButton = within(running).getByRole('button', { name: en['clear.pending'] })
|
||||
// The pressed control keeps the focus and reads as busy; disabling it would
|
||||
// hand focus to the document body mid-removal.
|
||||
expect(runningButton.getAttribute('aria-disabled')).toBe('true')
|
||||
expect(document.activeElement).toBe(runningButton)
|
||||
expect(within(running).getByRole('button', { name: en['delete.cancel'] }).hasAttribute('disabled')).toBe(true)
|
||||
// A second press while the removal runs must not start another request.
|
||||
fireEvent.click(runningButton)
|
||||
expect(onClearEnded).toHaveBeenCalledOnce()
|
||||
// The confirmation is the only place that states the removal still runs, so
|
||||
// the Escape that would close it waits for the outcome.
|
||||
fireEvent.keyDown(running, { key: 'Escape' })
|
||||
expect(screen.getByRole('dialog')).toBe(running)
|
||||
|
||||
await act(async () => { pending.resolve(undefined) })
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
expect(document.activeElement).toBe(trigger)
|
||||
})
|
||||
|
||||
it('falls back to the page heading when the settled cleanup hides its trigger', async () => {
|
||||
const pending = Promise.withResolvers<undefined>()
|
||||
const onClearEnded = vi.fn(() => pending.promise)
|
||||
const h = mount({ records: [at, endedTask] }, en, { onClearEnded })
|
||||
const trigger = moreTrigger()
|
||||
trigger.focus()
|
||||
const dialog = openClear()
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: en['clear.confirm'] }))
|
||||
// The acknowledged removal's readback drops the ended rows, and with them the
|
||||
// trigger the Modal captured when it opened.
|
||||
act(() => { h.update({ records: [at] }) })
|
||||
expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull()
|
||||
await act(async () => { pending.resolve(undefined) })
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 }))
|
||||
})
|
||||
|
||||
it('keeps focus in the confirmation when the selected task disappears behind it', () => {
|
||||
const h = mount({ records: [at, endedTask] })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Ended summary' }))
|
||||
expect(screen.getByRole('complementary', { name: en['detail.label'] })).toBeDefined()
|
||||
const dialog = openClear()
|
||||
// A refresh drops the selected task while the confirmation is open; closing
|
||||
// the detail must not take the focus out of the dialog.
|
||||
act(() => { h.update({ records: [at] }) })
|
||||
expect(screen.queryByRole('complementary', { name: en['detail.label'] })).toBeNull()
|
||||
expect(dialog.contains(document.activeElement)).toBe(true)
|
||||
fireEvent.keyDown(dialog, { key: 'Escape' })
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
// The row that held the focus is gone with the detail, so the heading takes it.
|
||||
expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 }))
|
||||
})
|
||||
|
||||
it('falls back to the page heading when another cleanup removes the trigger', () => {
|
||||
const h = mount({ records: [at, endedTask] })
|
||||
const trigger = moreTrigger()
|
||||
trigger.focus()
|
||||
const dialog = openClear()
|
||||
// Another client's cleanup removes the ended rows while this confirmation
|
||||
// is open, so the control the Modal captured is unmounted too.
|
||||
act(() => { h.update({ records: [at] }) })
|
||||
expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull()
|
||||
fireEvent.keyDown(dialog, { key: 'Escape' })
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 }))
|
||||
})
|
||||
|
||||
it('focuses the rebuilt trigger when the dialog closes', () => {
|
||||
const h = mount({ records: [at, endedTask] })
|
||||
const dialog = openClear()
|
||||
// Another client clears the ended rows, then a delivery ends one more while
|
||||
// the confirmation stays open: the trigger unmounts and a new one mounts.
|
||||
act(() => { h.update({ records: [at] }) })
|
||||
act(() => { h.update({ records: [at, anotherEnded] }) })
|
||||
const rebuilt = moreTrigger()
|
||||
fireEvent.keyDown(dialog, { key: 'Escape' })
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
expect(document.activeElement).toBe(rebuilt)
|
||||
})
|
||||
|
||||
it('moves focus to the heading when a late readback removes the trigger', async () => {
|
||||
const pending = Promise.withResolvers<undefined>()
|
||||
const onClearEnded = vi.fn(() => pending.promise)
|
||||
const h = mount({ records: [at, endedTask] }, en, { onClearEnded })
|
||||
const trigger = moreTrigger()
|
||||
trigger.focus()
|
||||
const dialog = openClear()
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: en['clear.confirm'] }))
|
||||
// The removal settles while the catalog still lists the ended row: the
|
||||
// dialog closes and focus returns to the trigger.
|
||||
await act(async () => { pending.resolve(undefined) })
|
||||
expect(document.activeElement).toBe(trigger)
|
||||
// A readback superseded by `schedule/changed` publishes afterwards and takes
|
||||
// the trigger with its row; focus must not stay on the document body.
|
||||
act(() => { h.update({ records: [at] }) })
|
||||
expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 }))
|
||||
})
|
||||
|
||||
it('closes only the confirmation on Escape while a task detail is open', () => {
|
||||
mount({ records: [at, endedTask] })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Review release' }))
|
||||
const detail = screen.getByRole('complementary', { name: en['detail.label'] })
|
||||
const dialog = openClear()
|
||||
fireEvent.keyDown(dialog, { key: 'Escape' })
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
expect(screen.getByRole('complementary', { name: en['detail.label'] })).toBe(detail)
|
||||
})
|
||||
|
||||
it('states the cleanup and its confirmation in Chinese', () => {
|
||||
mount({ records: [at, endedTask] }, zh)
|
||||
const dialog = openClear(zh, 1)
|
||||
expect(within(dialog).getByText(zh['clear.description'])).toBeDefined()
|
||||
expect(within(dialog).getByRole('button', { name: zh['clear.confirm'] })).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe.each([['English', en], ['Chinese', zh]] as const)('task detail views in %s', (_name, dictionary) => {
|
||||
it('opens Rules under the selected task and separates rule fields from saved deliveries', async () => {
|
||||
const labels = detailViewLabels(dictionary)
|
||||
@@ -1551,7 +1775,7 @@ describe('Task manager details', () => {
|
||||
expect(screen.getByRole('heading', { level: 1, name: '自动化任务' })).toBeDefined()
|
||||
expect(screen.getByRole('searchbox', { name: '搜索任务' })).toBeDefined()
|
||||
const statuses = screen.getByRole('group', { name: '任务状态' })
|
||||
expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe('全部')
|
||||
expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe('已开启')
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Check metrics' }))
|
||||
const detail = screen.getByRole('complementary', { name: '任务详情' })
|
||||
expect(within(detail).getByText(zh['rule.everySeconds'])).toBeDefined()
|
||||
@@ -3814,11 +4038,12 @@ describe('Mock detail metrics shared with the task list', () => {
|
||||
)
|
||||
// The Plugins toolbar's Add-plugin capsule, repeated by the creation action.
|
||||
expect(rule('creationActions')).toMatch(/gap:\s*16px;/)
|
||||
const newButton = rule('newButton')
|
||||
expect(newButton).toMatch(/height:\s*32px;/)
|
||||
expect(newButton).toMatch(/font-size:\s*13px;/)
|
||||
expect(newButton).toMatch(/padding:\s*0 12px;/)
|
||||
expect(newButton).toMatch(/border-radius:\s*16px;/)
|
||||
// The New action and the bulk cleanup share this capsule geometry.
|
||||
const headerAction = rule('headerAction')
|
||||
expect(headerAction).toMatch(/height:\s*32px;/)
|
||||
expect(headerAction).toMatch(/font-size:\s*13px;/)
|
||||
expect(headerAction).toMatch(/padding:\s*0 12px;/)
|
||||
expect(headerAction).toMatch(/border-radius:\s*16px;/)
|
||||
const headingTitle = /\.pageHeading h1\s*\{([^}]*)\}/.exec(stylesheet)?.[1] ?? ''
|
||||
// The first-level title step is the Plugins page's `pageTitle`, not the
|
||||
// mock's larger page heading; the equality case below pins that pair.
|
||||
@@ -3834,9 +4059,14 @@ describe('Mock detail metrics shared with the task list', () => {
|
||||
expect(tab).toMatch(/font-size:\s*14px;/)
|
||||
// The selected chip keeps the chip's own weight; only the fill marks it.
|
||||
expect(rule('filterTabActive')).not.toMatch(/font-weight/)
|
||||
// The mock's `.searchbox` keeps its hairline at the repository's 0.5px.
|
||||
// The search row fixes the mock's 36px control height for the field and the
|
||||
// square More trigger, and keeps the hairline at the repository's 0.5px.
|
||||
expect(rule('searchTools')).toMatch(/--task-search-height:\s*36px;/)
|
||||
expect(rule('catalogMore')).toMatch(/width:\s*var\(--task-search-height\);/)
|
||||
expect(rule('catalogMore')).toMatch(/height:\s*var\(--task-search-height\);/)
|
||||
expect(rule('catalogMore')).toMatch(/padding:\s*0;/)
|
||||
const field = rule('searchField')
|
||||
expect(field).toMatch(/height:\s*36px;/)
|
||||
expect(field).toMatch(/height:\s*var\(--task-search-height\);/)
|
||||
expect(field).toMatch(/padding:\s*0 10px;/)
|
||||
expect(field).toMatch(/border:\s*0\.5px solid var\(--dsw-alias-border-l3\);/)
|
||||
expect(field).toMatch(/border-radius:\s*12px;/)
|
||||
|
||||
@@ -285,14 +285,19 @@
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sidebarButton {
|
||||
display: inline-flex;
|
||||
.sidebarButton,
|
||||
.sidebarSpace {
|
||||
box-sizing: border-box;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.sidebarButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
border: 0;
|
||||
border-radius: var(--dsw-radius-sm);
|
||||
|
||||
@@ -353,24 +353,26 @@ function CatalogRows({
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{!isCurrent && !external && (
|
||||
<Tooltip label={t('open.sidebar')} side="bottom" align="end">
|
||||
<button
|
||||
type="button"
|
||||
className={css.sidebarButton}
|
||||
aria-label={t('open.sidebar.aria', { label })}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
openChildAside({ parentSessionId, childSessionId: entry.id, mode: entry.mode })
|
||||
closeCatalog()
|
||||
}}
|
||||
onKeyDown={(event) => { event.stopPropagation() }}
|
||||
>
|
||||
<IconChevronRightOutlineRegular />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isCurrent || external
|
||||
? <span className={css.sidebarSpace} />
|
||||
: (
|
||||
<Tooltip label={t('open.sidebar')} side="bottom" align="end">
|
||||
<button
|
||||
type="button"
|
||||
className={css.sidebarButton}
|
||||
aria-label={t('open.sidebar.aria', { label })}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
openChildAside({ parentSessionId, childSessionId: entry.id, mode: entry.mode })
|
||||
closeCatalog()
|
||||
}}
|
||||
onKeyDown={(event) => { event.stopPropagation() }}
|
||||
>
|
||||
<IconChevronRightOutlineRegular />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
|
||||
@@ -1099,6 +1099,8 @@ describe('SubagentHeaderLineage', () => {
|
||||
const current = screen.getByRole('treeitem', { name: /worker/ })
|
||||
expect(current.getAttribute('aria-current')).toBe('true')
|
||||
expect(within(current).getByText('worker').className).toContain('currentLabel')
|
||||
expect(within(current).queryByRole('button', { name: '在侧边栏打开 worker' })).toBeNull()
|
||||
expect(within(screen.getByRole('treeitem', { name: /reviewer/ })).getByRole('button', { name: '在侧边栏打开 reviewer' })).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('treeitem', { name: /reviewer/ }))
|
||||
expect(input.openChild).toHaveBeenCalledWith({
|
||||
parentSessionId: PARENT,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
".leading: border-radius: 10px"
|
||||
],
|
||||
"ui-schedule/src/client/TaskManagerPage.module.css": [
|
||||
".newButton: border-radius: 16px",
|
||||
".headerAction: border-radius: 16px",
|
||||
".filterTab: border-radius: 14px",
|
||||
".searchField: border-radius: 12px",
|
||||
".row: border-radius: 12px",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
/* Full-bleed, fixed-height host for the trajectory ledger. */
|
||||
/* Full-bleed, fixed-height host for the trajectory ledger. Isolation keeps the
|
||||
sticky toolbar's z-index inside this view, so the conversation tab row
|
||||
stacked above the view still paints its active bar over the view's top
|
||||
edge. */
|
||||
.root {
|
||||
--dsh-trajectory-toolbar-height: 32px;
|
||||
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -5,63 +5,63 @@
|
||||
/:
|
||||
en: 1a68a7a733a2676d
|
||||
zh: 01f72eb61a0626b7
|
||||
/deepseek-ai-dsh-experimental-action-cards:
|
||||
en: e57ce09e58e6fabc
|
||||
zh: 8a727dbd6f925363
|
||||
/deepseek-ai-dsh-experimental-action-cards/summary:
|
||||
/deepseek-ai-dsh-action-cards:
|
||||
en: 63bf9077e164f6fe
|
||||
zh: 8b696bb8c3d6fcd4
|
||||
/deepseek-ai-dsh-action-cards/summary:
|
||||
en: 7621088773d073f4
|
||||
zh: 57674e6dcee15997
|
||||
/deepseek-ai-dsh-experimental-action-cards/table-of-contents:
|
||||
/deepseek-ai-dsh-action-cards/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: b00dddf4c88f0a45
|
||||
/deepseek-ai-dsh-experimental-action-cards/use-this-package:
|
||||
en: 5041f4b6aa4fd119
|
||||
zh: 1bed53f562f622fa
|
||||
/deepseek-ai-dsh-experimental-action-cards/use-this-package/register-a-card-kind:
|
||||
/deepseek-ai-dsh-action-cards/use-this-package:
|
||||
en: 4e3a256d5aa6d29b
|
||||
zh: b89ffa23ebb49e7e
|
||||
/deepseek-ai-dsh-action-cards/use-this-package/register-a-card-kind:
|
||||
en: 8432c02ef18eda11
|
||||
zh: 6d43a0a32edd7e30
|
||||
/deepseek-ai-dsh-experimental-action-cards/use-this-package/card-rules:
|
||||
/deepseek-ai-dsh-action-cards/use-this-package/card-rules:
|
||||
en: 2f5b3ccb91f3b30e
|
||||
zh: 9bd877d9d92615da
|
||||
/deepseek-ai-dsh-experimental-action-cards/understand-the-implementation:
|
||||
/deepseek-ai-dsh-action-cards/understand-the-implementation:
|
||||
en: d92243d71d2106c1
|
||||
zh: 47f0840fa723bb31
|
||||
/deepseek-ai-dsh-experimental-action-cards/further-exploration:
|
||||
/deepseek-ai-dsh-action-cards/further-exploration:
|
||||
en: 1966ab8d7063c450
|
||||
zh: 033c2c0d258a257f
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience:
|
||||
/deepseek-ai-dsh-action-cards/model-experience:
|
||||
en: 215e7ba838619b7b
|
||||
zh: a311da3843709f90
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience/card-tool:
|
||||
/deepseek-ai-dsh-action-cards/model-experience/card-tool:
|
||||
en: 4925bc5f7153825a
|
||||
zh: 3a7bfb2210d9c6aa
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience/card-tool/what-the-model-sees:
|
||||
en: 58a76216203226f7
|
||||
zh: 0a12b579f6b28f29
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience/card-tool/token-effect:
|
||||
/deepseek-ai-dsh-action-cards/model-experience/card-tool/what-the-model-sees:
|
||||
en: 58521d3992ce42ec
|
||||
zh: 45681f7b43e67613
|
||||
/deepseek-ai-dsh-action-cards/model-experience/card-tool/token-effect:
|
||||
en: 7efaaad09be98a07
|
||||
zh: 86c7fa13d60c3588
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience/card-tool/kv-cache-effect:
|
||||
/deepseek-ai-dsh-action-cards/model-experience/card-tool/kv-cache-effect:
|
||||
en: d4192e35d47b5fa3
|
||||
zh: c786828e123de657
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience/card-status-notice:
|
||||
/deepseek-ai-dsh-action-cards/model-experience/card-status-notice:
|
||||
en: 407ebca5eb94992e
|
||||
zh: 779819ea83974103
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience/card-status-notice/what-the-model-sees:
|
||||
/deepseek-ai-dsh-action-cards/model-experience/card-status-notice/what-the-model-sees:
|
||||
en: a14013ca3ddb5ee3
|
||||
zh: 18cd7ffc62fd6590
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience/card-status-notice/what-the-model-sees/notice-preamble:
|
||||
/deepseek-ai-dsh-action-cards/model-experience/card-status-notice/what-the-model-sees/notice-preamble:
|
||||
en: 04dd9469bfaff052
|
||||
zh: 35110b231b8e3928
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience/card-status-notice/token-effect:
|
||||
/deepseek-ai-dsh-action-cards/model-experience/card-status-notice/token-effect:
|
||||
en: 4e73a2cff60b5e88
|
||||
zh: 4887212815e7425d
|
||||
/deepseek-ai-dsh-experimental-action-cards/model-experience/card-status-notice/kv-cache-effect:
|
||||
/deepseek-ai-dsh-action-cards/model-experience/card-status-notice/kv-cache-effect:
|
||||
en: 58785543e2428dba
|
||||
zh: 4b082f77b3b2f482
|
||||
/deepseek-ai-dsh-experimental-action-cards/known-limitations-and-deferred-work:
|
||||
/deepseek-ai-dsh-action-cards/known-limitations-and-deferred-work:
|
||||
en: 163aa0c2a6b7e3f8
|
||||
zh: 26cdcae33579114a
|
||||
/deepseek-ai-dsh-experimental-action-cards/known-limitations-and-deferred-work/dev-note:
|
||||
/deepseek-ai-dsh-action-cards/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: bd8a8936229e035e
|
||||
@@ -3,7 +3,7 @@ description: "Shared Host service of optional action cards: the propose_action t
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-experimental-action-cards
|
||||
# @deepseek-ai/dsh-action-cards
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
@@ -31,7 +31,7 @@ The [Personalized pages](../creation-recommendations/README.md) and [Plugin reco
|
||||
|---|---|---|
|
||||
| `queuedMessageLimit` | `2` | Queued next-turn user messages at or above which `propose_action` refuses the call, so no card is offered. |
|
||||
|
||||
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-action-cards) is the exhaustive source for accepted fields.
|
||||
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-action-cards) is the exhaustive source for accepted fields.
|
||||
|
||||
### Register a card kind
|
||||
|
||||
@@ -85,7 +85,7 @@ The Remote `act(sessionId, callId, verb, payload?)` checks the owner, the live r
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The [`propose_action` declaration](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-action-cards) appears only to a live root Agent whose own direct call loaded the skill of a registered kind in this Session, from the next step on, for that Agent's lifetime. Subagents never see it, and neither do PTC presets, whose skill loads are dispatched from code, so they show no cards. A kind's skill names the action and the fields of `details`. An accepted call returns `Card shown below your answer. Do not restate it. <invitation>`, where `<invitation>` is the kind's one- or two-sentence guidance on inviting the user to the card in the user's language, so the model writes an invitation only after a card exists. A refused call carries no invitation and returns its reason, for example `Load skill <skill> first, then call propose_action.`, `One card per turn; the earlier card stands.`, `The user has queued more messages; show no card and answer briefly.`, `Cards are offered in the main conversation only.`, `Unknown action "<action>". Registered actions: <actions>.`, where the list reads `none` once the last kind unregistered, or the failing `details` fields.
|
||||
The [`propose_action` declaration](../../../docs/tool-catalog.md#deepseek-aidsh-action-cards) appears only to a live root Agent whose own direct call loaded the skill of a registered kind in this Session, from the next step on, for that Agent's lifetime. Subagents never see it, and neither do PTC presets, whose skill loads are dispatched from code, so they show no cards. A kind's skill names the action and the fields of `details`. An accepted call returns `Card shown below your answer. Do not restate it. <invitation>`, where `<invitation>` is the kind's one- or two-sentence guidance on inviting the user to the card in the user's language, so the model writes an invitation only after a card exists. A refused call carries no invitation and returns its reason, for example `Load skill <skill> first, then call propose_action.`, `One card per turn; the earlier card stands.`, `The user has queued more messages; show no card and answer briefly.`, `Cards are offered in the main conversation only.`, `Unknown action "<action>". Registered actions: <actions>.`, where the list reads `none` once the last kind unregistered, or the failing `details` fields.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ description: "可选操作卡片的共享 Host 服务:propose_action 工具、
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-experimental-action-cards
|
||||
# @deepseek-ai/dsh-action-cards
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
@@ -31,7 +31,7 @@ kind: "package-reference"
|
||||
|---|---|---|
|
||||
| `queuedMessageLimit` | `2` | 排队到后续轮次的用户消息达到该数量时,`propose_action` 拒绝调用,因此不会展示卡片。 |
|
||||
|
||||
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-action-cards)是可接受字段的完整来源。
|
||||
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-action-cards)是可接受字段的完整来源。
|
||||
|
||||
### 注册卡片种类
|
||||
|
||||
@@ -85,7 +85,7 @@ Remote `act(sessionId, callId, verb, payload?)` 先检查服务所有者、在
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
[`propose_action` 声明](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-action-cards)只对在本会话中自己直接调用加载过已注册种类 Skill 的在线根 Agent 出现,从下一步开始,持续到该 Agent 被释放。subagent 看不到它;PTC 预设的 Skill 加载由代码发起,也看不到它,因此不会展示卡片。种类的 Skill 给出动作名和 `details` 的字段。被接受的调用返回 `Card shown below your answer. Do not restate it. <invitation>`,其中 `<invitation>` 是种类给出的一两句指引,说明如何用用户的语言邀请用户使用卡片,因此模型只会在卡片已经存在之后写邀请。被拒绝的调用不带邀请,只返回原因,例如 `Load skill <skill> first, then call propose_action.`、`One card per turn; the earlier card stands.`、`The user has queued more messages; show no card and answer briefly.`、`Cards are offered in the main conversation only.`、`Unknown action "<action>". Registered actions: <actions>.`(最后一个种类注销后列表为 `none`),或未通过校验的 `details` 字段。
|
||||
[`propose_action` 声明](../../../docs/tool-catalog.zh.md#deepseek-aidsh-action-cards)只对在本会话中自己直接调用加载过已注册种类 Skill 的在线根 Agent 出现,从下一步开始,持续到该 Agent 被释放。subagent 看不到它;PTC 预设的 Skill 加载由代码发起,也看不到它,因此不会展示卡片。种类的 Skill 给出动作名和 `details` 的字段。被接受的调用返回 `Card shown below your answer. Do not restate it. <invitation>`,其中 `<invitation>` 是种类给出的一两句指引,说明如何用用户的语言邀请用户使用卡片,因此模型只会在卡片已经存在之后写邀请。被拒绝的调用不带邀请,只返回原因,例如 `Load skill <skill> first, then call propose_action.`、`One card per turn; the earlier card stands.`、`The user has queued more messages; show no card and answer briefly.`、`Cards are offered in the main conversation only.`、`Unknown action "<action>". Registered actions: <actions>.`(最后一个种类注销后列表为 `none`),或未通过校验的 `details` 字段。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-experimental-action-cards",
|
||||
"name": "@deepseek-ai/dsh-action-cards",
|
||||
"description": "Optional action cards below answers: one shared proposal tool, card state, user actions and status notices",
|
||||
"version": "0.2.1-rc.2",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -5,36 +5,36 @@
|
||||
/:
|
||||
en: eec43de9002f4ab1
|
||||
zh: 0d2fddca5a1f993a
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards:
|
||||
en: 6ed1f2bf10bc2f1b
|
||||
zh: e3edea71ec4373c7
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/summary:
|
||||
/deepseek-ai-dsh-client-ui-action-cards:
|
||||
en: c36a554629f96f79
|
||||
zh: 0ea837da87938e52
|
||||
/deepseek-ai-dsh-client-ui-action-cards/summary:
|
||||
en: 89ea57734c42a481
|
||||
zh: c7a4f5f0b59b78ad
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/table-of-contents:
|
||||
/deepseek-ai-dsh-client-ui-action-cards/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: b00dddf4c88f0a45
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/use-this-package:
|
||||
/deepseek-ai-dsh-client-ui-action-cards/use-this-package:
|
||||
en: 163d385e91a5df38
|
||||
zh: 95bdde9e844cca51
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/use-this-package/register-a-card-body:
|
||||
/deepseek-ai-dsh-client-ui-action-cards/use-this-package/register-a-card-body:
|
||||
en: 9ec9f9f8f3165d3e
|
||||
zh: 2d8154ec2fe0c9ab
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/understand-the-implementation:
|
||||
/deepseek-ai-dsh-client-ui-action-cards/understand-the-implementation:
|
||||
en: 168ae39a83d6f4eb
|
||||
zh: 312cc12876bca834
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/further-exploration:
|
||||
/deepseek-ai-dsh-client-ui-action-cards/further-exploration:
|
||||
en: 55e4d74a33cdeeb3
|
||||
zh: f601d5a4dcf63ec5
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/model-experience:
|
||||
/deepseek-ai-dsh-client-ui-action-cards/model-experience:
|
||||
en: f80a4b656c108f22
|
||||
zh: a3e30f46fa7136ac
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/model-experience/kv-cache-effect:
|
||||
/deepseek-ai-dsh-client-ui-action-cards/model-experience/kv-cache-effect:
|
||||
en: 9cb7131bde146341
|
||||
zh: 1c7d1af00f58004d
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/known-limitations-and-deferred-work:
|
||||
/deepseek-ai-dsh-client-ui-action-cards/known-limitations-and-deferred-work:
|
||||
en: df94821940e4e2b8
|
||||
zh: 490441ab06b153c9
|
||||
/deepseek-ai-dsh-experimental-client-ui-action-cards/known-limitations-and-deferred-work/dev-note:
|
||||
/deepseek-ai-dsh-client-ui-action-cards/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: bd8a8936229e035e
|
||||
@@ -3,7 +3,7 @@ description: "Browser half of optional action cards: the card frame below comple
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-experimental-client-ui-action-cards
|
||||
# @deepseek-ai/dsh-client-ui-action-cards
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ description: "可选操作卡片的浏览器端:已完成回答下方的卡片
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-experimental-client-ui-action-cards
|
||||
# @deepseek-ai/dsh-client-ui-action-cards
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"license": "MIT",
|
||||
"name": "@deepseek-ai/dsh-experimental-client-ui-action-cards",
|
||||
"name": "@deepseek-ai/dsh-client-ui-action-cards",
|
||||
"description": "Action cards below completed answers: the shared card frame, kind bodies and the compact tool row",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -57,7 +57,7 @@
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-tool": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"@deepseek-ai/dsh-experimental-action-cards": "workspace:*",
|
||||
"@deepseek-ai/dsh-action-cards": "workspace:*",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
|
||||
"@deepseek-ai/dsh-util-values": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:*"
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import type {
|
||||
ActionCardDecision, ActionCardFields, ActionCardProposal, ActionCardResult, ActionCardView,
|
||||
} from '@deepseek-ai/dsh-experimental-action-cards/types'
|
||||
} from '@deepseek-ai/dsh-action-cards/types'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-chat/client'
|
||||
import type { ActionCardKey } from './locales.ts'
|
||||
import type { FoldedTextProps, RegisteredBody } from './registry.ts'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Bind generated Remote descriptors to the source-tested browser lifecycle. */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import remote from '@deepseek-ai/dsh-experimental-action-cards/remote'
|
||||
import remote from '@deepseek-ai/dsh-action-cards/remote'
|
||||
import { mountActionCards } from './mount.ts'
|
||||
export { inject } from './mount.ts'
|
||||
export type { ActionCardBodyProps, ActionCardDescriptor, ActionCardRegistry, FoldedTextProps } from './registry.ts'
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-session/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-tool/client'
|
||||
import type {} from '@deepseek-ai/dsh-experimental-action-cards/remote'
|
||||
import type {} from '@deepseek-ai/dsh-action-cards/remote'
|
||||
import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { ActionCardTail, type TailInjected } from './Card.tsx'
|
||||
import { ProposeRow } from './ProposeRow.tsx'
|
||||
@@ -46,7 +46,7 @@ export async function mountActionCards(ctx: Context, contribution: TypertRemoteC
|
||||
function registerUi(ctx: Context, registry: ActionCardRegistry): void {
|
||||
ctx.effect(() => ctx.locale.register('actionCards', { en, zh, ja }), 'action-cards: locale')
|
||||
ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({
|
||||
name: 'conversation.chat.turnTail', id: '@deepseek-ai/dsh-experimental-client-ui-action-cards', locale: 'actionCards',
|
||||
name: 'conversation.chat.turnTail', id: '@deepseek-ai/dsh-client-ui-action-cards', locale: 'actionCards',
|
||||
inject: (sessionId: SessionId): TailInjected => ({
|
||||
sessionId,
|
||||
hooks: { bodies: registry.source },
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { HostObservable, LocaleNamespaceMap, Translate, TranslateNS } from
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type {
|
||||
ActionCardDecision, ActionCardFields, ActionCardProposal, ActionCardResult,
|
||||
} from '@deepseek-ai/dsh-experimental-action-cards/types'
|
||||
} from '@deepseek-ai/dsh-action-cards/types'
|
||||
|
||||
/** Props of the frame's folded text, which bodies reuse for their own long fields. */
|
||||
export interface FoldedTextProps {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { mountActionCards, inject } from '../src/client/mount.ts'
|
||||
import { apply as hostApply } from '../src/index.ts'
|
||||
import type { TailInjected } from '../src/client/Card.tsx'
|
||||
|
||||
const apply = (ctx: Context) => mountActionCards(ctx, { package: '@deepseek-ai/dsh-experimental-action-cards', descriptors: [] })
|
||||
const apply = (ctx: Context) => mountActionCards(ctx, { package: '@deepseek-ai/dsh-action-cards', descriptors: [] })
|
||||
const Body = () => null
|
||||
|
||||
it('binds actions to the displayed session, serves registered bodies and removes registrations on unload', async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types'
|
||||
import type { ActionCardDecision, ActionCardResult, ActionCardView } from '@deepseek-ai/dsh-experimental-action-cards/types'
|
||||
import type { ActionCardDecision, ActionCardResult, ActionCardView } from '@deepseek-ai/dsh-action-cards/types'
|
||||
import { ActionCard, ActionCardTail, FoldedText } from '../src/client/Card.tsx'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { en, zh } from '../src/client/locales.ts'
|
||||
|
||||
@@ -5,36 +5,36 @@
|
||||
/:
|
||||
en: e332dc3625021ad9
|
||||
zh: a6637d7d8696ba5a
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations:
|
||||
en: dc8e9b2fafade59a
|
||||
zh: fab88019b957e3d9
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/summary:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations:
|
||||
en: b4af39c5d085f16e
|
||||
zh: 19fd311580db7e2b
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/summary:
|
||||
en: c2b93e3c50f80ecd
|
||||
zh: da8aa68e686abddd
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/table-of-contents:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: b00dddf4c88f0a45
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/use-this-package:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/use-this-package:
|
||||
en: 5f3f7e9eea09ae0c
|
||||
zh: 378acca8f793cf34
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/use-this-package/outcomes-and-recovery:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/use-this-package/outcomes-and-recovery:
|
||||
en: 5a3df9cd58832f91
|
||||
zh: 2068dca0a62394fe
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/understand-the-implementation:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/understand-the-implementation:
|
||||
en: 26b049c5e3f38846
|
||||
zh: 15768ae54aec3511
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/further-exploration:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/further-exploration:
|
||||
en: 6543c44016da2cdf
|
||||
zh: fb0d365a5e411b04
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/model-experience:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/model-experience:
|
||||
en: 176de8a02931b042
|
||||
zh: b1f1228f011ab056
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/model-experience/kv-cache-effect:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/model-experience/kv-cache-effect:
|
||||
en: 6a708541b49367ae
|
||||
zh: 5722b25a6a8e6724
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/known-limitations-and-deferred-work:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/known-limitations-and-deferred-work:
|
||||
en: 93594f90162451d5
|
||||
zh: ca27b35469b537be
|
||||
/deepseek-ai-dsh-experimental-client-ui-creation-recommendations/known-limitations-and-deferred-work/dev-note:
|
||||
/deepseek-ai-dsh-client-ui-creation-recommendations/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 8ec3e73528ddb1a6
|
||||
@@ -3,7 +3,7 @@ description: "Show completed-answer creation cards with location choice, Keep an
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-experimental-client-ui-creation-recommendations
|
||||
# @deepseek-ai/dsh-client-ui-creation-recommendations
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ description: "在完整回答末尾显示支持位置选择、保留和撤销的
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-experimental-client-ui-creation-recommendations
|
||||
# @deepseek-ai/dsh-client-ui-creation-recommendations
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"license": "MIT",
|
||||
"name": "@deepseek-ai/dsh-experimental-client-ui-creation-recommendations",
|
||||
"name": "@deepseek-ai/dsh-client-ui-creation-recommendations",
|
||||
"description": "Body of the personalized page card: location choice, creation progress, Keep, Undo and Restore",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -32,7 +32,7 @@
|
||||
"platform": "web",
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-experimental-client-ui-action-cards"
|
||||
"@deepseek-ai/dsh-client-ui-action-cards"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -46,10 +46,10 @@
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"@deepseek-ai/dsh-experimental-creation-recommendations": "workspace:*",
|
||||
"@deepseek-ai/dsh-creation-recommendations": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-store": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-client-ui-action-cards": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-action-cards": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-action-cards": "workspace:*",
|
||||
"@deepseek-ai/dsh-action-cards": "workspace:*",
|
||||
"zod": "4.4.3",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
|
||||
@@ -4,8 +4,8 @@ import { z } from 'zod'
|
||||
import { Button, IconChevronDownOutlineRegular, IconWarningTriangleOutlineRegular, Menu, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { ActionCardBodyProps } from '@deepseek-ai/dsh-experimental-client-ui-action-cards/client'
|
||||
import type { CreationSlot, PageData, PageDetails, PageVerb } from '@deepseek-ai/dsh-experimental-creation-recommendations/types'
|
||||
import type { ActionCardBodyProps } from '@deepseek-ai/dsh-client-ui-action-cards/client'
|
||||
import type { CreationSlot, PageData, PageDetails, PageVerb } from '@deepseek-ai/dsh-creation-recommendations/types'
|
||||
import type { RecommendationKey } from './locales.ts'
|
||||
import css from './Card.module.css'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { createElement } from 'react'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type {} from '@deepseek-ai/dsh-experimental-client-ui-action-cards/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-action-cards/client'
|
||||
import { PageCardBody } from './Card.tsx'
|
||||
import { en, ja, zh, type RecommendationKey } from './locales.ts'
|
||||
import { createCreationInfoStore } from './info-store.ts'
|
||||
|
||||
@@ -4,8 +4,8 @@ import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { FoldedText } from '@deepseek-ai/dsh-experimental-client-ui-action-cards/src/client/Card.tsx'
|
||||
import type { ActionCardFields, ActionCardResult } from '@deepseek-ai/dsh-experimental-action-cards/types'
|
||||
import { FoldedText } from '@deepseek-ai/dsh-client-ui-action-cards/src/client/Card.tsx'
|
||||
import type { ActionCardFields, ActionCardResult } from '@deepseek-ai/dsh-action-cards/types'
|
||||
import { PageCardBody, type PageCardProps } from '../src/client/Card.tsx'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { en, zh } from '../src/client/locales.ts'
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { ActionCardDescriptor } from '@deepseek-ai/dsh-experimental-client-ui-action-cards/client'
|
||||
import { FoldedText } from '@deepseek-ai/dsh-experimental-client-ui-action-cards/src/client/Card.tsx'
|
||||
import type { ActionCardDescriptor } from '@deepseek-ai/dsh-client-ui-action-cards/client'
|
||||
import { FoldedText } from '@deepseek-ai/dsh-client-ui-action-cards/src/client/Card.tsx'
|
||||
import { afterEach, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { apply, inject } from '../src/client/mount.ts'
|
||||
import { apply as hostApply } from '../src/index.ts'
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@deepseek-ai/dsh-api-remotes",
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
||||
"@deepseek-ai/dsh-experimental-client-ui-action-cards"
|
||||
"@deepseek-ai/dsh-client-ui-action-cards"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -55,8 +55,8 @@
|
||||
"@deepseek-ai/dsh-client-ui-plugin-manager": "workspace:*",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
|
||||
"@deepseek-ai/dsh-package-manifest": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-client-ui-action-cards": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-action-cards": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-action-cards": "workspace:*",
|
||||
"@deepseek-ai/dsh-action-cards": "workspace:*",
|
||||
"zod": "4.4.3",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { LocalizedText } from '@deepseek-ai/dsh-package-manifest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import type { ActionCardBodyProps } from '@deepseek-ai/dsh-experimental-client-ui-action-cards/client'
|
||||
import type { ActionCardBodyProps } from '@deepseek-ai/dsh-client-ui-action-cards/client'
|
||||
import type { PluginDetails, RecommendationDetails } from '@deepseek-ai/dsh-experimental-plugin-recommendations/types'
|
||||
import type { RecommendationKey } from './locales.ts'
|
||||
import css from './Card.module.css'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
|
||||
import type {} from '@deepseek-ai/dsh-experimental-client-ui-action-cards/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-action-cards/client'
|
||||
import type {} from '@deepseek-ai/dsh-experimental-plugin-recommendations/remote'
|
||||
import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { PluginCardBody, type CatalogDetails, type PluginCardServices } from './Card.tsx'
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { ActionCardDescriptor } from '@deepseek-ai/dsh-experimental-client-ui-action-cards/client'
|
||||
import { FoldedText } from '@deepseek-ai/dsh-experimental-client-ui-action-cards/src/client/Card.tsx'
|
||||
import type { ActionCardDescriptor } from '@deepseek-ai/dsh-client-ui-action-cards/client'
|
||||
import { FoldedText } from '@deepseek-ai/dsh-client-ui-action-cards/src/client/Card.tsx'
|
||||
import { afterEach, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { mountRecommendations, inject } from '../src/client/mount.ts'
|
||||
import { apply as hostApply } from '../src/index.ts'
|
||||
|
||||
Loaded 100 of 168 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user