Merge updated DeepSeek onboarding base
# Conflicts: # packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
@@ -19,6 +20,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/message-actions', import
|
||||
// a new recording (workspace-management / sidebar-scrollbar pattern).
|
||||
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
|
||||
const FORK_EXPECTED = join(SNAPSHOT_DIR, 'fork.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'message-actions-web-e2e'
|
||||
|
||||
@@ -85,9 +87,58 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('forks through the settled-message and session-row actions', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-fork'))
|
||||
// Exercise the assistant action specifically; package coverage pins the
|
||||
// user action separately at its own event seq.
|
||||
await page.getByRole('button', { name: '在新对话中分支' }).last().click()
|
||||
await expect.poll(
|
||||
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SEED_ID)),
|
||||
{ timeout: 15_000 },
|
||||
).toBeDefined()
|
||||
await expect.poll(
|
||||
() => page.locator('[role="treeitem"]').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(3)
|
||||
await expect.poll(
|
||||
() => page.locator('[role="treeitem"][aria-selected="true"]').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(1)
|
||||
// The row action owns a distinct ui-workspace injection from the message
|
||||
// action above, so exercise both through the loaded app before capture.
|
||||
const sourceRow = page.locator('[role="treeitem"][aria-selected="true"]')
|
||||
const rowBox = await sourceRow.boundingBox()
|
||||
if (rowBox === null) throw new Error('fork source row has no layout box')
|
||||
const actionButton = sourceRow.locator('button[aria-label^="Session actions for "]')
|
||||
await sourceRow.hover({ position: { x: rowBox.width - 16, y: rowBox.height / 2 } })
|
||||
await expect.poll(() => actionButton.isVisible(), { timeout: 2_000 }).toBe(true)
|
||||
const buttonBox = await actionButton.boundingBox()
|
||||
if (buttonBox === null) throw new Error('fork source row action has no layout box')
|
||||
await page.mouse.click(buttonBox.x + buttonBox.width / 2, buttonBox.y + buttonBox.height / 2)
|
||||
await page.getByRole('menuitem', { name: 'Fork session' }).click()
|
||||
await expect.poll(
|
||||
() => scaffold.ctx.agents.list().filter(agent => agent.session.header.parentSession !== undefined).length,
|
||||
{ timeout: 15_000 },
|
||||
).toBe(2)
|
||||
await expect.poll(
|
||||
() => page.locator('[role="treeitem"]').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(4)
|
||||
await expect.poll(
|
||||
() => page.locator('[role="treeitem"][aria-selected="true"]').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(1)
|
||||
const tree = await captureStableAria(
|
||||
page,
|
||||
'[role="tree"][aria-label="Sessions"]',
|
||||
scaffold.workspaceCwd,
|
||||
)
|
||||
await compareOrRefreshGolden(FORK_EXPECTED, tree, MODE)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('issued zero model calls and kept a closed inventory', async () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['fork.expected.md', 'ui.expected.md'])
|
||||
})
|
||||
})
|
||||
|
||||
113
apps/web/tests/plan-review.e2e.ts
Normal file
113
apps/web/tests/plan-review.e2e.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// Web e2e scenario: the plan-review takeover. The shipped composition mounts
|
||||
// plan mode and its client seat, so `/plan <task>` enters plan mode for real
|
||||
// and the recorded turn ends on exit_plan_mode blocking against the live
|
||||
// userInteraction seam. The composer is then occupied by the plan decision
|
||||
// card — not the generic question flow — and approving it through the card
|
||||
// completes the turn with the approval in the log.
|
||||
// Replay is deterministic: the plan content arrives from replayed chunks, the
|
||||
// review wait is real, and the approve click is the test's own gesture (the
|
||||
// turn cannot complete without it, in record and replay alike).
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/plan-review', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
// The waiting golden owns the decision card; the approved golden owns the
|
||||
// transcript the approval leaves behind — the state the card cannot see.
|
||||
const REVIEW_EXPECTED = join(SNAPSHOT_DIR, 'review.expected.md')
|
||||
const APPROVED_EXPECTED = join(SNAPSHOT_DIR, 'approved.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
// One command line: /plan enters plan mode and submits the rest as the turn's
|
||||
// message. The task is deliberately self-contained (nothing to explore in a
|
||||
// fresh workspace) so the recorded turn is a plan and its review, and the
|
||||
// approved continuation is one word.
|
||||
const TASK = 'Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. '
|
||||
+ 'Call exit_plan_mode with a short plan of at most five bullet points. '
|
||||
+ 'Once the plan is approved, reply with the single word DONE and stop.'
|
||||
const LINE = `/plan ${TASK}`
|
||||
|
||||
describe('web e2e: plan review takeover round trip', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 })
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
// English page: the decision copy is the surface under test, and the
|
||||
// golden pins one language.
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('reviews the plan on a decision card and approves through it', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-plan-review'))
|
||||
if (MODE !== 'record') {
|
||||
expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([TASK])
|
||||
}
|
||||
const input = page.locator('textarea').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
const settled = scaffold.whenTurnSettled(MODE === 'record' ? 180_000 : 30_000)
|
||||
await input.fill(LINE)
|
||||
await input.press('Enter')
|
||||
|
||||
// The card takes over the input area while exit_plan_mode blocks. Its
|
||||
// presence is a STABLE waiting state (it stays until answered), so a plain
|
||||
// waitFor is race-free.
|
||||
const card = page.locator('[data-plan-review-key]')
|
||||
await card.waitFor({ timeout: MODE === 'record' ? 120_000 : 30_000 })
|
||||
// The plan-review request must NOT land on the generic question flow.
|
||||
expect(await page.locator('[data-question-key]').count()).toBe(0)
|
||||
await expect.poll(() => card.getByText('Plan review').count(), { timeout: 10_000 }).toBeGreaterThan(0)
|
||||
|
||||
if (MODE !== 'record') {
|
||||
const snapshot = await captureStableAria(page, '[data-plan-review-key]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(REVIEW_EXPECTED, snapshot, MODE)
|
||||
}
|
||||
|
||||
await card.getByRole('button', { name: 'Approve' }).click()
|
||||
|
||||
const sessionId = await settled
|
||||
if (MODE === 'record') {
|
||||
await recordFixture(scaffold, sessionId, FIXTURE)
|
||||
return
|
||||
}
|
||||
// World state: the approval reached the tool, and plan mode is left behind.
|
||||
const results = sessionEvents.filter(e => e.type === 'tool/result')
|
||||
expect(JSON.stringify(results.at(-1))).toContain('Plan approved')
|
||||
await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(1)
|
||||
// Card gone; regular input restored.
|
||||
expect(await page.locator('[data-plan-review-key]').count()).toBe(0)
|
||||
await expect.poll(() => page.locator('textarea').first().isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(APPROVED_EXPECTED, snapshot, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 200_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'review.expected.md', 'approved.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -20,6 +20,7 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/queue-actions', import.meta.url))
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/live-interactions/session.jsonl', import.meta.url))
|
||||
const COLLAPSED_EXPECTED = join(SNAPSHOT_DIR, 'collapsed.expected.md')
|
||||
const EDITING_EXPECTED = join(SNAPSHOT_DIR, 'editing.expected.md')
|
||||
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -80,6 +81,16 @@ describe('web e2e: queue row actions', () => {
|
||||
await input.fill(text)
|
||||
await input.press('Enter')
|
||||
}
|
||||
const queueHeader = page.getByRole('button', { name: '2 条排队消息' })
|
||||
await expect.poll(() => queueHeader.getAttribute('aria-expanded'), { timeout: 10_000 })
|
||||
.toBe('false')
|
||||
const collapsedSnapshot = await captureStableAria(
|
||||
page,
|
||||
'[class*="centerCol"]',
|
||||
scaffold.workspaceCwd,
|
||||
)
|
||||
await compareOrRefreshGolden(COLLAPSED_EXPECTED, collapsedSnapshot, MODE)
|
||||
await queueHeader.click()
|
||||
await expect.poll(
|
||||
() => page.getByRole('button', { name: '删除排队消息' }).count(),
|
||||
{ timeout: 10_000 },
|
||||
@@ -112,6 +123,9 @@ describe('web e2e: queue row actions', () => {
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps its snapshot inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['editing.expected.md', 'ui.expected.md'])
|
||||
await assertFixtureInventory(
|
||||
SNAPSHOT_DIR,
|
||||
['collapsed.expected.md', 'editing.expected.md', 'ui.expected.md'],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
57
apps/web/tests/scaffold-hermetic.e2e.ts
Normal file
57
apps/web/tests/scaffold-hermetic.e2e.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import type {} from '@deepseek-ai/dsh-skill'
|
||||
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
|
||||
|
||||
async function writeSkill(root: string, name: string): Promise<void> {
|
||||
const bundle = join(root, name)
|
||||
await mkdir(bundle, { recursive: true })
|
||||
await writeFile(join(bundle, 'SKILL.md'), `---
|
||||
name: ${name}
|
||||
description: Must not enter the Web replay scaffold
|
||||
---
|
||||
|
||||
Ambient host state.
|
||||
`)
|
||||
}
|
||||
|
||||
it('isolates replay skill discovery from every ambient host root', async () => {
|
||||
const ambient = await mkdtemp(join(tmpdir(), 'dsh-web-ambient-skills-'))
|
||||
const dshHome = join(ambient, 'dsh-home')
|
||||
const agentsHome = join(ambient, 'agents-home')
|
||||
const bundled = join(ambient, 'bundled')
|
||||
await Promise.all([
|
||||
writeSkill(join(dshHome, 'skills'), 'ambient-dsh'),
|
||||
writeSkill(join(agentsHome, 'skills'), 'ambient-agents'),
|
||||
writeSkill(bundled, 'ambient-bundled'),
|
||||
])
|
||||
|
||||
const originalDshHome = process.env.DSH_HOME
|
||||
const originalAgentsHome = process.env.DSH_AGENTS_HOME
|
||||
const originalBundled = process.env.DSH_BUNDLED_SKILL_DIR
|
||||
process.env.DSH_HOME = dshHome
|
||||
process.env.DSH_AGENTS_HOME = agentsHome
|
||||
process.env.DSH_BUNDLED_SKILL_DIR = bundled
|
||||
let scaffold: WebScaffold | undefined
|
||||
try {
|
||||
scaffold = await launchWebScaffold()
|
||||
const names = (await scaffold.ctx.skills.list({ cwd: scaffold.workspaceCwd })).map(skill => skill.name)
|
||||
expect(names).not.toContain('ambient-dsh')
|
||||
expect(names).not.toContain('ambient-agents')
|
||||
expect(names).not.toContain('ambient-bundled')
|
||||
} finally {
|
||||
try {
|
||||
await scaffold?.close()
|
||||
} finally {
|
||||
if (originalDshHome === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = originalDshHome
|
||||
if (originalAgentsHome === undefined) delete process.env.DSH_AGENTS_HOME
|
||||
else process.env.DSH_AGENTS_HOME = originalAgentsHome
|
||||
if (originalBundled === undefined) delete process.env.DSH_BUNDLED_SKILL_DIR
|
||||
else process.env.DSH_BUNDLED_SKILL_DIR = originalBundled
|
||||
await rm(ambient, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -11,13 +11,14 @@
|
||||
// masking its credential, without making a model call.
|
||||
//
|
||||
// Composition divergences from `dsh web`, all deliberate, all via include
|
||||
// patches after the shipped surface overlay: temp persistenceRoot;
|
||||
// workspace-context disabled (recorded fixtures must not embed this repo's
|
||||
// AGENTS.md); session-title-llm disabled (its fire-and-forget title call
|
||||
// would race the loop for the session's replay cursor); webserver pinned to
|
||||
// port 0 with the built dist; ordinary keyless modes disable llm-deepseek and
|
||||
// fill the open llm seam post-boot with installLlmReplay on the settled root
|
||||
// ctx (the plugin-row path discards the ReplayHandle; the direct install keeps
|
||||
// patches after the shipped surface overlay: temp persistenceRoot; local skill
|
||||
// roots confined to the temp workspace; workspace-context disabled (recorded
|
||||
// fixtures must not embed this repo's AGENTS.md); session-title-llm disabled
|
||||
// (its fire-and-forget title call would race the loop for the session's replay
|
||||
// cursor); webserver pinned to port 0 with the built dist; ordinary keyless
|
||||
// modes disable llm-deepseek and fill the open llm seam post-boot with
|
||||
// installLlmReplay on the settled root ctx
|
||||
// (the plugin-row path discards the ReplayHandle; the direct install keeps
|
||||
// assertConsumed for the teardown fixture-consumption check).
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises'
|
||||
@@ -202,6 +203,19 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// per write; the scaffold restores the original cwd after boot, so the
|
||||
// row gets an absolute temp root (removed with the workspace at close).
|
||||
{ id: 'storage-json', config: { root: join(workspaceCwd, '.dsh-storages') } },
|
||||
// Skill discovery is model-visible input. Pin every host-level root inside
|
||||
// the owned temp world so ~/.dsh, ~/.agents, and a bundled-root env setting
|
||||
// cannot change replay requests or conversation goldens. Project roots stay
|
||||
// enabled against the same empty temp workspace, preserving the real seam.
|
||||
{
|
||||
id: 'skill-local',
|
||||
config: {
|
||||
dshHome: join(workspaceCwd, '.dsh-home'),
|
||||
agentsHome: join(workspaceCwd, '.agents-home'),
|
||||
bundledSkillDir: join(workspaceCwd, '.bundled-skills'),
|
||||
watch: false,
|
||||
},
|
||||
},
|
||||
// fs/bash cwd default to process.cwd(); the gateway injects the same
|
||||
// value into session.cwd — chdir below anchors all three to the temp
|
||||
// workspace, keeping the composition untouched.
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- 'button "Think The user wants me to write a single `run_code` program that:"':
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- button "Think The user wants me to:":
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- button "Think The user wants me to run a simple bash command and reply with \"DONE\".":
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- button "Think The user wants me to reply with a single word. Let me comply.":
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- paragraph: partial
|
||||
- text: 已停止
|
||||
- button "复制":
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.":
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
- tree "Sessions":
|
||||
- treeitem "Ungrouped 3 sessions" [expanded]:
|
||||
- img
|
||||
- text: Ungrouped 3 sessions
|
||||
- treeitem "Use the read tool twice (2) now" [selected]
|
||||
- treeitem "Use the read tool twice (1) now"
|
||||
- treeitem "Use the read tool twice 1min"
|
||||
51
apps/web/tests/snapshots/plan-review/approved.expected.md
Normal file
51
apps/web/tests/snapshots/plan-review/approved.expected.md
Normal file
@@ -0,0 +1,51 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Plan a small change: add" [disabled]'
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- img
|
||||
- text: "/plan Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. Plan mode on. Use /plan off to leave. Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- 'button "Think The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."':
|
||||
- img
|
||||
- img
|
||||
- text: "Think The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."
|
||||
- paragraph:
|
||||
- text: Since the user has explicitly asked me not to read or write any files and to go straight to planning, I'll proceed with
|
||||
- code: exit_plan_mode
|
||||
- text: .
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
- text: "Tool call exit_plan_mode · # Add `--greeting` flag to CLI"
|
||||
- 'button "Think The plan was approved. The user''s last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop."':
|
||||
- img
|
||||
- img
|
||||
- text: "Think The plan was approved. The user's last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop."
|
||||
- paragraph: DONE
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 51% Input 10.2K tok · Output 346 tok
|
||||
44
apps/web/tests/snapshots/plan-review/review.expected.md
Normal file
44
apps/web/tests/snapshots/plan-review/review.expected.md
Normal file
@@ -0,0 +1,44 @@
|
||||
- region "Approve this plan and leave plan mode?":
|
||||
- text: Plan review
|
||||
- heading "Add --greeting flag to CLI" [level=1]:
|
||||
- text: Add
|
||||
- code: "--greeting"
|
||||
- text: flag to CLI
|
||||
- list:
|
||||
- listitem:
|
||||
- strong: Locate the CLI entry point
|
||||
- text: (e.g.,
|
||||
- code: cli.py
|
||||
- text: ","
|
||||
- code: main.go
|
||||
- text: ","
|
||||
- code: index.js
|
||||
- text: etc.) and find the argument parser definition (argparse, click, cobra, yargs, or similar).
|
||||
- listitem:
|
||||
- strong: Register a new optional string argument
|
||||
- text: named
|
||||
- code: "--greeting"
|
||||
- text: with a short alias (
|
||||
- code: "-g"
|
||||
- text: if available) and a sensible default value (e.g.,
|
||||
- code: "\"Hello\""
|
||||
- text: ).
|
||||
- listitem:
|
||||
- strong: Thread the parsed value
|
||||
- text: through the main handler function so it is passed where the greeting string is used (e.g., the welcome/response message).
|
||||
- listitem:
|
||||
- strong: Update the help text
|
||||
- text: so
|
||||
- code: "--help"
|
||||
- text: or
|
||||
- code: "-h"
|
||||
- text: shows the new flag with its description.
|
||||
- listitem:
|
||||
- strong: No tests or config changes
|
||||
- text: unless they already exist and directly validate the flag's presence.
|
||||
- status
|
||||
- button "Chat about it":
|
||||
- img
|
||||
- text: Chat about it
|
||||
- button "Refuse"
|
||||
- button "Approve"
|
||||
39
apps/web/tests/snapshots/plan-review/session.jsonl
Normal file
39
apps/web/tests/snapshots/plan-review/session.jsonl
Normal file
@@ -0,0 +1,39 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785406804293,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"command/run","seq":0,"time":1785406804350,"data":{"commandId":"cmd-228a60ef-1","name":"plan","args":" Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop.","source":{"kind":"user"}}}
|
||||
{"type":"plan/mode","seq":1,"time":1785406804350,"data":{"active":true}}
|
||||
{"type":"command/done","seq":2,"time":1785406804352,"data":{"commandId":"cmd-228a60ef-1","kind":"success","text":"Plan mode on. Use /plan off to leave."}}
|
||||
{"type":"turn/start","seq":3,"time":1785406804353,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":4,"time":1785406804353,"data":{"content":[{"type":"text","text":"Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"a22bd0d2-15dc-4bfb-b978-0d5961459e57"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":5,"time":1785406804354,"data":{"title":"Plan a small change: add","messageSeqs":[4],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":6,"time":1785406804355,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":7,"time":1785406804356,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash","reasoningEffort":"high"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":8,"time":1785406805697,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":9,"time0":1785406805698,"data":{"turn":1,"step":1,"index":0,"dt":[151,31,0,0,0,0,18,1,0,0,0,31,1,0,0,0,1,23,0,0,0,19,0,0,37,1,0,0,0,0,12,1,0,0,28,0,1,0,24,1,0,0,0,0,26,1,0,0,26,1,22],"texts":["The"," user"," wants"," me"," to"," plan"," a"," small"," change"," to"," add"," a"," `","--","gre","eting","`"," flag"," to"," a"," CLI","."," They"," explicitly"," told"," me"," not"," to"," read"," or"," write"," any"," files",","," and"," to"," call"," exit","_","plan","_mode"," with"," a"," short"," plan","."," Let"," me"," do"," that"," directly","."]}}
|
||||
{"type":"assistant/chunk","seq":61,"time":1785406806155,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"text-chunks","seq0":62,"time0":1785406806156,"data":{"turn":1,"step":1,"index":1,"dt":[0,20,0,34,23,26,0,1,0,0,28,0,0,0,32,0,15,1,0,30,1,0,0,23,1,0,24,0,0,0],"texts":["Since"," the"," user"," has"," explicitly"," asked"," me"," not"," to"," read"," or"," write"," any"," files"," and"," to"," go"," straight"," to"," planning",","," I","'ll"," proceed"," with"," `","exit","_","plan","_mode","`."]}}
|
||||
{"type":"assistant/chunk","seq":93,"time":1785406806493,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":94,"time0":1785406806493,"data":{"turn":1,"step":1,"index":2,"dt":[27,1,0,0,0,24,0,0,29,1,0,0,0,1,20,0,27,1,0,21,28,0,0,31,1,23,0,0,1,23,0,0,0,23,1,0,28,20,26,28,1,0,29,1,0,0,21,28,0,0,25,1,26,1,0,0,24,1,0,26,0,0,0,1,23,1,0,0,22,29,0,31,1,1,0,21,1,0,0,0,0,20,0,32,25,0,0,22,0,0,27,0,1,0,31,0,1,0,0,20,1,0,0,0,0,24,0,1,0,26,1,29,1,26,1,0,25,30,1,18,0,30,0,0,30,21,1,28,0,21,1,0,0,23,1,23,36,0,0,1,0,13,1,28,27,0,0,25,1,45,1,11,0,21,0,0,0,0,23,0,0,32,1,0,0,0,16,28,0,27,1,0,22,31,1,24,1,21,32,1,0,24,1,0,24],"id":"call_00_TPh5fH8x5kRezK115kgx8155","name":"exit_plan_mode","args":["","{","\"","plan","\"",": ","\"","#"," Add"," `","--","gre","eting","`"," flag"," to"," CLI","\\n\\n","-"," **","Loc","ate"," the"," CLI"," entry"," point","**"," (","e",".g",".,"," `","cli",".py","`,"," `","main",".go","`,"," `","index",".js","`"," etc",".)"," and"," find"," the"," argument"," parser"," definition"," (","arg","parse",","," click",","," cob","ra",","," y","args",","," or"," similar",").\\n","-"," **","Register"," a"," new"," optional"," string"," argument","**"," named"," `","--","gre","eting","`"," with"," a"," short"," alias"," (`","-","g","`"," if"," available",")"," and"," a"," sensible"," default"," value"," (","e",".g",".,"," `","\\\"","Hello","\\\"","`",").\\n","-"," **","Thread"," the"," parsed"," value","**"," through"," the"," main"," handler"," function"," so"," it"," is"," passed"," where"," the"," greeting"," string"," is"," used"," (","e",".g",".,"," the"," welcome","/","response"," message",").\\n","-"," **","Update"," the"," help"," text","**"," so"," `","--","help","`"," or"," `","-","h","`"," shows"," the"," new"," flag"," with"," its"," description",".\\n","-"," **","No"," tests"," or"," config"," changes","**"," unless"," they"," already"," exist"," and"," directly"," validate"," the"," flag","'s"," presence",".","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":280,"time":1785406808318,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."}}}}
|
||||
{"type":"assistant/chunk","seq":281,"time":1785406808318,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"Since the user has explicitly asked me not to read or write any files and to go straight to planning, I'll proceed with `exit_plan_mode`."}}}}
|
||||
{"type":"assistant/chunk","seq":282,"time":1785406808318,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"call_00_TPh5fH8x5kRezK115kgx8155","name":"exit_plan_mode","arguments":"{\"plan\": \"# Add `--greeting` flag to CLI\\n\\n- **Locate the CLI entry point** (e.g., `cli.py`, `main.go`, `index.js` etc.) and find the argument parser definition (argparse, click, cobra, yargs, or similar).\\n- **Register a new optional string argument** named `--greeting` with a short alias (`-g` if available) and a sensible default value (e.g., `\\\"Hello\\\"`).\\n- **Thread the parsed value** through the main handler function so it is passed where the greeting string is used (e.g., the welcome/response message).\\n- **Update the help text** so `--help` or `-h` shows the new flag with its description.\\n- **No tests or config changes** unless they already exist and directly validate the flag's presence.\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":283,"time":1785406808319,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":4529,"outputTokens":303,"cacheReadTokens":640,"reasoningTokens":52}}}}
|
||||
{"type":"assistant/chunk","seq":284,"time":1785406808319,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":285,"time":1785406808324,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."},{"type":"text","text":"Since the user has explicitly asked me not to read or write any files and to go straight to planning, I'll proceed with `exit_plan_mode`."},{"type":"tool-call","id":"call_00_TPh5fH8x5kRezK115kgx8155","name":"exit_plan_mode","arguments":"{\"plan\": \"# Add `--greeting` flag to CLI\\n\\n- **Locate the CLI entry point** (e.g., `cli.py`, `main.go`, `index.js` etc.) and find the argument parser definition (argparse, click, cobra, yargs, or similar).\\n- **Register a new optional string argument** named `--greeting` with a short alias (`-g` if available) and a sensible default value (e.g., `\\\"Hello\\\"`).\\n- **Thread the parsed value** through the main handler function so it is passed where the greeting string is used (e.g., the welcome/response message).\\n- **Update the help text** so `--help` or `-h` shows the new flag with its description.\\n- **No tests or config changes** unless they already exist and directly validate the flag's presence.\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"b9e18116-68f6-4442-b5a5-a33277ef526c"},"usage":{"inputTokens":4529,"outputTokens":303,"cacheReadTokens":640,"reasoningTokens":52}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":286,"time":1785406808325,"data":{"turn":1,"step":1,"callId":"call_00_TPh5fH8x5kRezK115kgx8155","name":"exit_plan_mode","arguments":"{\"plan\": \"# Add `--greeting` flag to CLI\\n\\n- **Locate the CLI entry point** (e.g., `cli.py`, `main.go`, `index.js` etc.) and find the argument parser definition (argparse, click, cobra, yargs, or similar).\\n- **Register a new optional string argument** named `--greeting` with a short alias (`-g` if available) and a sensible default value (e.g., `\\\"Hello\\\"`).\\n- **Thread the parsed value** through the main handler function so it is passed where the greeting string is used (e.g., the welcome/response message).\\n- **Update the help text** so `--help` or `-h` shows the new flag with its description.\\n- **No tests or config changes** unless they already exist and directly validate the flag's presence.\"}"}}
|
||||
{"type":"tool/result","seq":287,"time":1785406808720,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_TPh5fH8x5kRezK115kgx8155"},"content":[{"type":"tool-result","toolCallId":"call_00_TPh5fH8x5kRezK115kgx8155","content":[{"type":"text","text":"Plan approved — plan mode exited; carry out the plan starting with your next step."}],"isError":false}],"role":"user","id":"e7cf1f9f-e143-48df-b975-440a9d61a538"}},"sourceEventSeqs":[286],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":288,"time":1785406808721,"data":{"turn":1,"step":1}}
|
||||
{"type":"plan/mode","seq":289,"time":1785406808721,"data":{"active":false}}
|
||||
{"type":"step/start","seq":290,"time":1785406808723,"data":{"turn":1,"step":2}}
|
||||
{"type":"request/header","seq":291,"time":1785406808723,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash","reasoningEffort":"high"},"system":"{{system}}","tools":"{{tools}}"},"reason":"change"}}
|
||||
{"type":"assistant/chunk","seq":292,"time":1785406809855,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":293,"time0":1785406809855,"data":{"turn":1,"step":2,"index":0,"dt":[133,28,1,0,0,17,0,28,1,0,0,29,0,0,0,0,0,21,0,0,0,35,0,0,0,0,0,14,1,37,1,0,1,17,0,0,0,0,1],"texts":["The"," plan"," was"," approved","."," The"," user","'s"," last"," instruction"," says",":"," \"","Once"," the"," plan"," is"," approved",","," reply"," with"," the"," single"," word"," D","ONE"," and"," stop",".\""," So"," I"," should"," just"," reply"," with"," D","ONE"," and"," stop","."]}}
|
||||
{"type":"assistant/chunk","seq":333,"time":1785406810252,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":334,"time":1785406810253,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}}
|
||||
{"type":"assistant/chunk","seq":335,"time":1785406810253,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}}
|
||||
{"type":"assistant/chunk","seq":336,"time":1785406810256,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The plan was approved. The user's last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop."}}}}
|
||||
{"type":"assistant/chunk","seq":337,"time":1785406810256,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":338,"time":1785406810256,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":441,"outputTokens":43,"cacheReadTokens":4608,"reasoningTokens":40}}}}
|
||||
{"type":"assistant/chunk","seq":339,"time":1785406810256,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":340,"time":1785406810257,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The plan was approved. The user's last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"2d9da552-d236-4166-9d06-dffa9bace4cd"},"usage":{"inputTokens":441,"outputTokens":43,"cacheReadTokens":4608,"reasoningTokens":40}},"sourceEventSeqs":[292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":341,"time":1785406810258,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":342,"time":1785406810258,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- button "Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that.":
|
||||
- img
|
||||
- img
|
||||
|
||||
24
apps/web/tests/snapshots/queue-actions/collapsed.expected.md
Normal file
24
apps/web/tests/snapshots/queue-actions/collapsed.expected.md
Normal file
@@ -0,0 +1,24 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- paragraph: partial
|
||||
- button "2 条排队消息"
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Stop generating"
|
||||
@@ -11,11 +11,8 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- paragraph: partial
|
||||
- button "2 条排队消息" [disabled] [expanded]
|
||||
- list:
|
||||
- listitem:
|
||||
- text: Queue item to remove
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- paragraph: partial
|
||||
- list:
|
||||
- listitem:
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
// cannot see both sides of the cordis Context merges).
|
||||
"exclude": [
|
||||
"tests/scaffold.ts",
|
||||
"tests/scaffold-hermetic.e2e.ts",
|
||||
"tests/live-interactions.e2e.ts",
|
||||
"tests/question-composer.e2e.ts",
|
||||
"tests/plan-review.e2e.ts",
|
||||
"tests/steering.e2e.ts",
|
||||
"tests/navigation-panes.e2e.ts",
|
||||
"tests/lifecycle-chrome.e2e.ts",
|
||||
|
||||
Reference in New Issue
Block a user