mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
Revert "feat(client): expose stable composer prefill API"
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.2-alpha.2/composer-prefill-method/guide.md
|
||||
/:
|
||||
en: a99282dc38a7dfeb
|
||||
zh: 6f6ee2170907eb26
|
||||
/composer-prefill-method-on-input-actions:
|
||||
en: 976e77fa72fdfdd2
|
||||
zh: dbaa50bd205dc1f3
|
||||
/composer-prefill-method-on-input-actions/change:
|
||||
en: 5186bc4ec9254340
|
||||
zh: ff0e406a96d5c50b
|
||||
/composer-prefill-method-on-input-actions/migration:
|
||||
en: 21625abfcb53c507
|
||||
zh: 4a6f0aaa04eb3a2c
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Client code that builds InputActions or implements SessionInput must provide the required prefill method."
|
||||
---
|
||||
|
||||
# Composer prefill method on input actions
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`InputActions` and `SessionInput` in `@deepseek-ai/dsh-client-ui-conversation/client` now require `prefill(text, options?)`, and the package exports `ComposerPrefillMode` and `ComposerPrefillOptions`. Slot components and Session-scope plugins read prefill from the harness, so their call sites compile unchanged. Code that constructs an `InputActions` value, or implements `SessionInput`, including test doubles passed to client components, no longer satisfies the interfaces until it adds the member. The method returns whether the draft accepted the write; returning `false` is valid for doubles that do not support prefill.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Add `prefill` to every hand-written `InputActions` or `SessionInput` object. Forward to the harness-provided action, or return `false` when the double does not support writing.
|
||||
2. Import `ComposerPrefillOptions` or `ComposerPrefillMode` from `@deepseek-ai/dsh-client-ui-conversation/client` when a signature needs them.
|
||||
3. Rebuild custom Client plugins and their test suites, then run the type check. A passing check confirms the interface is satisfied; call `inputActions.prefill('text')` from a slot component to verify the harness path when the object forwards the operation.
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "手写 InputActions 或实现 SessionInput 的 Client 代码必须提供必选方法 prefill。"
|
||||
---
|
||||
|
||||
# 输入操作上的 Composer 预填方法
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
`@deepseek-ai/dsh-client-ui-conversation/client` 中的 `InputActions` 和 `SessionInput` 现在要求提供 `prefill(text, options?)`,该包同时导出 `ComposerPrefillMode` 和 `ComposerPrefillOptions`。Slot 组件与 Session 作用域插件从 Harness 读取预填操作,调用方无需改动即可编译。构造 `InputActions` 值或实现 `SessionInput` 的代码(包括传给 Client 组件的测试替身)必须新增该成员才能满足接口。该方法返回草稿是否接受写入;不支持预填的替身可以返回 `false`。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 在所有手写的 `InputActions` 或 `SessionInput` 对象中加入 `prefill`。转发到 Harness 提供的操作,或在替身不支持写入时返回 `false`。
|
||||
2. 签名需要时,从 `@deepseek-ai/dsh-client-ui-conversation/client` 导入 `ComposerPrefillOptions` 或 `ComposerPrefillMode`。
|
||||
3. 重新构建自定义 Client 插件及其测试,然后运行类型检查。检查通过即表示接口已满足;当对象转发该操作时,可从 Slot 组件调用 `inputActions.prefill('text')` 验证 Harness 路径。
|
||||
@@ -525,7 +525,6 @@ function makeHarness(
|
||||
captureInsertion: () => ({ start: 0, end: 0, draftRev: 0 }),
|
||||
insertText: () => false,
|
||||
setDraft: () => {},
|
||||
prefill: () => false,
|
||||
persistDraft: () => {},
|
||||
addAttachments: () => true,
|
||||
removeAttachment: () => {},
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
en: 8811c28013fdc6dc
|
||||
zh: 0a86edc422b38ba8
|
||||
/deepseek-ai-dsh-client-ui-conversation/table-of-contents:
|
||||
en: c1ca3d169bea2ccc
|
||||
zh: 1892939a2ec76e1e
|
||||
en: eef4f18c3972ccc4
|
||||
zh: 6be65c6ac84ce497
|
||||
/deepseek-ai-dsh-client-ui-conversation/conversation-assembly:
|
||||
en: ef43c4f149195939
|
||||
zh: 49fc6c83fd3a05ce
|
||||
@@ -24,11 +24,8 @@
|
||||
en: 99ec447f0a6bda48
|
||||
zh: 922cf64666676530
|
||||
/deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries:
|
||||
en: 9acb3b1d7f279d97
|
||||
zh: 8544b7a140437553
|
||||
/deepseek-ai-dsh-client-ui-conversation/plugin-prefill-api:
|
||||
en: 0d16e9363a821bc6
|
||||
zh: eca2ba2076a3ca9d
|
||||
en: 59cd63057dfb6eeb
|
||||
zh: b2e2c86e810b7bb8
|
||||
/deepseek-ai-dsh-client-ui-conversation/model-experience:
|
||||
en: e0a05b73ba3e4318
|
||||
zh: dc85ab8433bb4456
|
||||
|
||||
@@ -19,7 +19,6 @@ Desktop product events use the optional [product analytics service](../product-a
|
||||
- [Sent user text](#sent-user-text)
|
||||
- [Shell and standard props](#shell-and-standard-props)
|
||||
- [Temporary composer entries](#temporary-composer-entries)
|
||||
- [Plugin prefill API](#plugin-prefill-api)
|
||||
- [Model Experience](#model-experience)
|
||||
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
||||
- [Dev Note](#dev-note)
|
||||
@@ -187,15 +186,6 @@ The selector must be a pure function of the owner currency. Its non-null return
|
||||
|
||||
Session title context gestures use the shared [context menu](../ui-context-menu/README.md) with the clicked breadcrumb’s Session. The reusable image gallery receives `getTriggerProps` from its Factory’s injected `useContextMenu` hook.
|
||||
|
||||
<a id="plugin-prefill-api"></a>
|
||||
## Plugin prefill API
|
||||
|
||||
A plugin can write reviewable text into the current composer through the stable `prefill` action. Slot components receive it as `inputActions.prefill(text, options)`. A session-scope plugin resolves the same action with `ctx.conversation.input.for(actx).prefill(text, options)`.
|
||||
|
||||
The exported `ComposerPrefillOptions` type and `InputActions.prefill` signature are defined in the [input contracts](src/client/contract/input.ts).
|
||||
|
||||
An empty string is ignored and returns `false`. An empty or whitespace-only draft, or `mode: 'replace'`, replaces the whole draft only while the composer is in the plain phase and the session is live; that replacement is its own undo step. Otherwise the text replaces the current selection in one undo step, preceded by `separator` when the editor text before the selection ends in non-whitespace; the separator defaults to a newline and an empty separator inserts verbatim. A rejected write — a non-plain replacement, a changed draft revision, a locked submission phase, or a disposed session — returns `false` without changing the draft. `prefill` never sends the draft.
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ kind: "package-reference"
|
||||
- [已发送用户文本](#sent-user-text)
|
||||
- [Shell 与标准 props](#shell-and-standard-props)
|
||||
- [临时 composer entry](#temporary-composer-entries)
|
||||
- [插件预填 API](#plugin-prefill-api)
|
||||
- [模型体验](#model-experience)
|
||||
- [已知限制与暂缓事项](#known-limitations-and-deferred-work)
|
||||
- [开发备注](#dev-note)
|
||||
@@ -187,15 +186,6 @@ selector 必须是 owner currency 的纯函数。非 null 返回值作为 `match
|
||||
|
||||
会话标题右键使用共享[右键菜单](../ui-context-menu/README.zh.md),目标是被点击面包屑所属会话。可复用图片库从其 Factory 注入的 `useContextMenu` hook 获取 `getTriggerProps`。
|
||||
|
||||
<a id="plugin-prefill-api"></a>
|
||||
## 插件预填 API
|
||||
|
||||
插件可以通过稳定的 `prefill` 操作把可检查的文本写入当前 Composer。Slot 组件收到的是 `inputActions.prefill(text, options)`;Session 作用域的插件通过 `ctx.conversation.input.for(actx).prefill(text, options)` 解析同一个操作。
|
||||
|
||||
导出的 `ComposerPrefillOptions` 类型和 `InputActions.prefill` 签名定义在[输入接口](src/client/contract/input.ts)中。
|
||||
|
||||
空字符串会被忽略并返回 `false`。草稿为空或只有空白,或者使用 `mode: 'replace'` 时,只有在 Composer 处于 plain 阶段且 Session 仍存活时才替换整个草稿;这次替换本身就是独立的撤销步骤。否则文本会替换当前选区,作为一次可撤销编辑;若选区前的编辑器文本末尾不是空白,则先插入 `separator`。`separator` 默认是换行,传空字符串时原样插入。非 plain 阶段的替换、草稿版本已变、提交阶段锁定或 Session 已释放时,写入会被拒绝并返回 `false`,草稿保持不变。`prefill` 不会发送草稿。
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -187,15 +187,6 @@ export interface SessionInput extends InputTarget {
|
||||
|
||||
/** Replace the whole draft with plain text, removing inline reference chips. */
|
||||
setDraft(text: string): void
|
||||
/**
|
||||
* Write plugin-owned text into the draft in one undo step. This is the
|
||||
* session-scope entry to the same prefill operation exposed as
|
||||
* {@link InputActions.prefill} to slot components.
|
||||
* @param text - text to write; an empty string is ignored.
|
||||
* @param options - replacement mode and insertion separator.
|
||||
* @returns whether the draft accepted the write.
|
||||
*/
|
||||
prefill(text: string, options?: ComposerPrefillOptions): boolean
|
||||
/** Append ordered browser-owned attachment ids; busy admission phases refuse. */
|
||||
addAttachments(ids: readonly DraftAttachmentId[]): boolean
|
||||
/** Remove one browser-owned attachment id; busy admission phases refuse. @returns whether the id was removed. */
|
||||
@@ -258,20 +249,6 @@ export interface DraftInitializationOptions {
|
||||
/** An initialization request never waits for a React mount or a remote catalog. */
|
||||
export type DraftInitializationResult = 'applied' | 'preserved' | 'blocked'
|
||||
|
||||
/** How a composer prefill treats existing draft text. */
|
||||
export type ComposerPrefillMode = 'insert' | 'replace'
|
||||
|
||||
/** Options for a plugin-owned composer prefill. */
|
||||
export interface ComposerPrefillOptions {
|
||||
/** Replace the whole draft instead of inserting at the caret; defaults to `insert`. */
|
||||
readonly mode?: ComposerPrefillMode
|
||||
/**
|
||||
* Text placed before an inserted prefill when the caret follows non-whitespace.
|
||||
* Defaults to a newline; an empty string inserts verbatim. Ignored when replacing.
|
||||
*/
|
||||
readonly separator?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The public input action face provided to every session-scope slot
|
||||
* component: stable-identity callbacks, mirroring the
|
||||
@@ -292,19 +269,6 @@ export interface InputActions {
|
||||
insertText(text: string, span: TokenSpan): boolean
|
||||
/** Replace the whole draft with plain text, removing inline reference chips. */
|
||||
setDraft(text: string): void
|
||||
/**
|
||||
* Write plugin-owned text into the draft in one undo step. An empty or
|
||||
* whitespace-only draft, or `mode: 'replace'`, replaces the draft only while
|
||||
* the session is live and the composer is in the plain phase; otherwise the
|
||||
* current selection is replaced by an insertion at the selection, preceded
|
||||
* by `separator` when the editor text before it ends in non-whitespace.
|
||||
* Replacement edits are independent undo steps.
|
||||
* @param text - text to write; an empty string is ignored.
|
||||
* @param options - replacement mode and insertion separator.
|
||||
* @returns whether the draft accepted the write; false for an empty string,
|
||||
* a non-plain replacement, a changed draft revision, or a disposed session.
|
||||
*/
|
||||
prefill(text: string, options?: ComposerPrefillOptions): boolean
|
||||
/** Write the current semantic document through the mounted persistence callback. */
|
||||
persistDraft(): void
|
||||
/** Append ordered browser-owned attachment ids; busy admission phases refuse. */
|
||||
|
||||
@@ -98,8 +98,7 @@ export type {
|
||||
UseConversationViews,
|
||||
} from './contract/slots.ts'
|
||||
export type {
|
||||
BeginCommandRequest, CommandClaim, ComposerPrefillMode, ComposerPrefillOptions,
|
||||
ConsumeTokenRequest, DraftAttachmentId, DraftInitializationOptions,
|
||||
BeginCommandRequest, CommandClaim, ConsumeTokenRequest, DraftAttachmentId, DraftInitializationOptions,
|
||||
DraftInitializationResult, InputActions,
|
||||
InputState, InsertReferenceRequest, InsertTextRequest, PickOutcome, SessionInput,
|
||||
SessionInputResolver, SubmitAttachment, SubmitOutcome,
|
||||
|
||||
@@ -205,26 +205,6 @@ export class DraftEditorRuntime {
|
||||
}, { discrete: true, tag: HISTORY_MERGE_TAG })
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the whole draft as one independent undo step at the top level.
|
||||
* Unlike the seed {@link setDraft}, this edit carries a history boundary so
|
||||
* the user can undo the plugin prefill without undoing an earlier seed or
|
||||
* edit. Nested calls join their enclosing editor transaction.
|
||||
* @param text - the full next draft.
|
||||
*/
|
||||
replaceDraft(text: string): void {
|
||||
const clean = text.replace(REFERENCE_PLACEHOLDER_RE, '')
|
||||
if (clean === this.projection.clipboardText && this.projection.occurrences.length === 0 && this.projection.quotes.length === 0) return
|
||||
// An empty editor has no earlier entry to undo to; seed one before the first prefill.
|
||||
if (this.editor.getEditorState().isEmpty()) {
|
||||
this.applyEdit(() => { $getRoot().append($createParagraphNode()) }, HISTORY_MERGE_TAG)
|
||||
}
|
||||
this.applyEdit(() => {
|
||||
$getRoot().clear()
|
||||
$appendParagraphs(clean)
|
||||
}, PASTE_TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add plain text after the current draft as new paragraphs, keeping its text
|
||||
* and references; an empty draft takes the text as its whole content.
|
||||
@@ -273,15 +253,9 @@ export class DraftEditorRuntime {
|
||||
* @returns the ordered [start, end) span in detect coordinates.
|
||||
*/
|
||||
caretSpan(): { start: number; end: number } {
|
||||
let span = { start: 0, end: 0 }
|
||||
// A snapshot read misses uncommitted native selection. A discrete update
|
||||
// synchronizes it; nested calls read the enclosing update's pending state.
|
||||
this.applyEdit(() => {
|
||||
const projection = $projectComposer(key => this.occurrenceIdOf(key))
|
||||
const at = projection.detectText.length
|
||||
span = projection.selection ?? { start: at, end: at }
|
||||
})
|
||||
return span
|
||||
if (this.projection.selection !== null) return this.projection.selection
|
||||
const at = this.projection.detectText.length
|
||||
return { start: at, end: at }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@deepseek-ai/dsh-client-store'
|
||||
import type { LexicalEditor } from 'lexical'
|
||||
import type {
|
||||
CommandClaim, ComposerPrefillOptions, ConsumeTokenRequest, DraftAttachmentId, DraftInitializationOptions, DraftInitializationResult,
|
||||
CommandClaim, ConsumeTokenRequest, DraftAttachmentId, DraftInitializationOptions, DraftInitializationResult,
|
||||
InputActions, InputEffect, InputNotice, InputState, InputTriggerController, PickOutcome,
|
||||
SessionInput, SubmitAttempt, SubmitAttachment, SubmitOutcome,
|
||||
} from '../contract/input.ts'
|
||||
@@ -148,7 +148,6 @@ export class SessionInputShell implements SessionInput {
|
||||
return this.draftEditor.insertAsyncText(span, text)
|
||||
},
|
||||
setDraft: (text) => { this.setDraft(text) },
|
||||
prefill: (text, options) => this.prefill(text, options),
|
||||
persistDraft: () => { this.persistCurrentDraft() },
|
||||
addAttachments: ids => this.addAttachments(ids),
|
||||
removeAttachment: (id) => { this.removeAttachment(id) },
|
||||
@@ -239,27 +238,6 @@ export class SessionInputShell implements SessionInput {
|
||||
return inserted
|
||||
}
|
||||
|
||||
/**
|
||||
* Write plugin-owned text into the draft in one undo step.
|
||||
* @param text - text to write; an empty string is ignored.
|
||||
* @param options - replacement mode and insertion separator.
|
||||
* @returns whether the draft accepted the write.
|
||||
*/
|
||||
prefill(text: string, options: ComposerPrefillOptions = {}): boolean {
|
||||
if (text.length === 0) return false
|
||||
if ((options.mode ?? 'insert') === 'replace' || this.snapshot.draft.trim() === '') {
|
||||
if (this.snapshot.phase !== 'plain' || this.disposed) return false
|
||||
this.cancelCommandRestore()
|
||||
this.draftEditor.replaceDraft(text)
|
||||
return true
|
||||
}
|
||||
const span = this.actions.captureInsertion()
|
||||
const separator = options.separator ?? '\n'
|
||||
const preceding = this.projection.detectText.slice(0, span.start)
|
||||
const prefix = separator.length > 0 && span.start > 0 && !/\s$/.test(preceding) ? separator : ''
|
||||
return this.actions.insertText(`${prefix}${text}`, span)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect optional catalog notifications and resume a pending command
|
||||
* chip restore without waiting for another edit.
|
||||
|
||||
@@ -818,44 +818,6 @@ describe('draft documents in the input shell', () => {
|
||||
)
|
||||
})
|
||||
|
||||
describe('plugin prefill history', () => {
|
||||
it.each(['insert', 'replace'] as const)('keeps the first %s prefill undoable before the editor mounts', async (mode) => {
|
||||
const shell = makeShell()
|
||||
expect(shell.editor.getRootElement()).toBeNull()
|
||||
expect(shell.prefill('First plugin draft', { mode })).toBe(true)
|
||||
shell.editor.dispatchCommand(UNDO_COMMAND, undefined)
|
||||
await vi.waitFor(() => { expect(shell.draftSnapshot).toEqual({ text: '', references: [] }) })
|
||||
shell.editor.dispatchCommand(REDO_COMMAND, undefined)
|
||||
await vi.waitFor(() => { expect(shell.draftSnapshot).toEqual({ text: 'First plugin draft', references: [] }) })
|
||||
})
|
||||
|
||||
it('keeps consecutive unmounted prefills as separate undo steps', async () => {
|
||||
const shell = makeShell()
|
||||
expect(shell.prefill('First')).toBe(true)
|
||||
expect(shell.prefill('Second', { mode: 'replace' })).toBe(true)
|
||||
shell.editor.dispatchCommand(UNDO_COMMAND, undefined)
|
||||
await vi.waitFor(() => { expect(shell.draftSnapshot).toEqual({ text: 'First', references: [] }) })
|
||||
shell.editor.dispatchCommand(UNDO_COMMAND, undefined)
|
||||
await vi.waitFor(() => { expect(shell.draftSnapshot).toEqual({ text: '', references: [] }) })
|
||||
shell.editor.dispatchCommand(REDO_COMMAND, undefined)
|
||||
await vi.waitFor(() => { expect(shell.draftSnapshot).toEqual({ text: 'First', references: [] }) })
|
||||
shell.editor.dispatchCommand(REDO_COMMAND, undefined)
|
||||
await vi.waitFor(() => { expect(shell.draftSnapshot).toEqual({ text: 'Second', references: [] }) })
|
||||
})
|
||||
|
||||
it('leaves an ordinary empty editor untouched until a prefill arrives', () => {
|
||||
const shell = makeShell()
|
||||
const root = document.createElement('div')
|
||||
root.contentEditable = 'true'
|
||||
document.body.append(root)
|
||||
onTestFinished(() => { shell.editor.setRootElement(null); root.remove() })
|
||||
shell.editor.setRootElement(root)
|
||||
expect(shell.prefill('')).toBe(false)
|
||||
expect(root.childElementCount).toBe(0)
|
||||
expect(shell.draftSnapshot).toEqual({ text: '', references: [] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('plain-text reference catalogs', () => {
|
||||
it('leaves named tokens plain until their own catalog includes them', () => {
|
||||
const unavailable = new Map<'/' | '@', readonly string[]>()
|
||||
|
||||
@@ -639,120 +639,3 @@ describe('scenario I: unknown /xyz + enter', () => {
|
||||
expect(b.sink).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('plugin composer prefill API', () => {
|
||||
it('fills empty drafts, inserts at the caret, and honors replace and separator options', async () => {
|
||||
const b = await bench()
|
||||
expect(b.shell.prefill('')).toBe(false)
|
||||
act(() => { expect(b.shell.prefill('Explain')).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('Explain')
|
||||
act(() => { expect(b.shell.actions.prefill('tests')).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('Explain\ntests')
|
||||
act(() => { expect(b.shell.prefill('next', { separator: ' ' })).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('Explain\ntests next')
|
||||
act(() => { b.shell.setDraft('Keep ') })
|
||||
act(() => { expect(b.shell.prefill('Next')).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('Keep Next')
|
||||
act(() => { expect(b.shell.prefill('Only', { mode: 'replace' })).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('Only')
|
||||
act(() => { expect(b.shell.prefill('Next', { separator: '' })).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('OnlyNext')
|
||||
act(() => {
|
||||
b.shell.editor.update(() => { $getRoot().selectStart() }, { discrete: true })
|
||||
})
|
||||
act(() => { expect(b.shell.prefill('Head')).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('HeadOnlyNext')
|
||||
})
|
||||
|
||||
it('reports a rejected insertion without changing the draft', async () => {
|
||||
const b = await bench()
|
||||
act(() => { b.shell.setDraft('Busy') })
|
||||
const insertText = vi.spyOn(b.shell.actions, 'insertText').mockReturnValue(false)
|
||||
act(() => { expect(b.shell.prefill('Next')).toBe(false) })
|
||||
expect(b.shell.snapshot.draft).toBe('Busy')
|
||||
insertText.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('plugin composer prefill API guards and history', () => {
|
||||
it('makes a replacement its own undo step', async () => {
|
||||
const b = await bench()
|
||||
act(() => { b.shell.setDraft('Old') })
|
||||
act(() => { expect(b.shell.prefill('New', { mode: 'replace' })).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('New')
|
||||
act(() => { b.shell.editor.dispatchCommand(UNDO_COMMAND, undefined) })
|
||||
await act(async () => {})
|
||||
expect(b.shell.snapshot.draft).toBe('Old')
|
||||
})
|
||||
|
||||
it('refuses replacement while sending and after disposal', async () => {
|
||||
const sending = await bench()
|
||||
act(() => { sending.shell.setDraft('Sent') })
|
||||
act(() => { sending.shell.submit() })
|
||||
expect(sending.shell.snapshot.phase).toBe('sending')
|
||||
act(() => { expect(sending.shell.prefill('New', { mode: 'replace' })).toBe(false) })
|
||||
|
||||
const disposed = await bench()
|
||||
act(() => { disposed.shell.setDraft('Keep') })
|
||||
disposed.shell.dispose()
|
||||
act(() => { expect(disposed.shell.prefill('New', { mode: 'replace' })).toBe(false) })
|
||||
act(() => { expect(disposed.shell.prefill('New')).toBe(false) })
|
||||
})
|
||||
|
||||
it('checks the separator against detect coordinates around a reference chip', async () => {
|
||||
const b = await bench()
|
||||
act(() => { b.shell.setDraft('@res') })
|
||||
const accepted = b.shell.insertReference({
|
||||
source: 'reference',
|
||||
ref: '@[Research](dsh-session:InNvdXJjZSI)',
|
||||
label: 'Research',
|
||||
clipboardText: '@[Research](dsh-session:InNvdXJjZSI)',
|
||||
}, {
|
||||
start: 0,
|
||||
end: 4,
|
||||
draftRev: b.shell.snapshot.draftRev,
|
||||
})
|
||||
expect(accepted).toBe(true)
|
||||
const afterChip = b.shell.snapshot.draft
|
||||
expect(afterChip.endsWith(' ')).toBe(true)
|
||||
act(() => { expect(b.shell.prefill('Next')).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe(`${afterChip}Next`)
|
||||
})
|
||||
|
||||
it('replaces a whitespace-only draft', async () => {
|
||||
const b = await bench()
|
||||
act(() => { b.shell.setDraft(' \n ') })
|
||||
act(() => { expect(b.shell.prefill('Text')).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('Text')
|
||||
})
|
||||
|
||||
it.each(['captureInsertion', 'prefill'] as const)('reads the native caret through %s before its selection is projected', async (entry) => {
|
||||
const b = await bench()
|
||||
act(() => { b.shell.setDraft('Keep text') })
|
||||
b.textarea.focus()
|
||||
const text = b.textarea.firstChild?.firstChild
|
||||
const selection = document.getSelection()
|
||||
if (text === undefined || text === null || selection === null) throw new Error('missing native draft selection')
|
||||
selection.setBaseAndExtent(text, 0, text, 0)
|
||||
const before = b.shell.draftSnapshot
|
||||
if (entry === 'captureInsertion') {
|
||||
const revision = b.shell.snapshot.draftRev
|
||||
const track = vi.spyOn(b.controller, 'track')
|
||||
const expected = { start: 0, end: 0, draftRev: revision }
|
||||
expect(b.shell.actions.captureInsertion()).toEqual(expected)
|
||||
expect(track).toHaveBeenCalledOnce()
|
||||
expect(b.shell.actions.captureInsertion()).toEqual(expected)
|
||||
expect(track).toHaveBeenCalledOnce()
|
||||
expect(b.shell.draftSnapshot).toBe(before)
|
||||
expect(b.shell.snapshot.draftRev).toBe(revision)
|
||||
expect(b.sink).not.toHaveBeenCalled()
|
||||
return
|
||||
}
|
||||
act(() => { expect(b.shell.prefill('Head', { separator: '' })).toBe(true) })
|
||||
expect(b.shell.snapshot.draft).toBe('HeadKeep text')
|
||||
act(() => { b.shell.editor.dispatchCommand(UNDO_COMMAND, undefined) })
|
||||
await act(async () => {})
|
||||
expect(b.shell.draftSnapshot).toEqual(before)
|
||||
expect(b.sink).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -242,7 +242,6 @@ const unusedFactoryProps = {
|
||||
captureInsertion: unusedFrameworkProp,
|
||||
insertText: unusedFrameworkProp,
|
||||
setDraft: unusedFrameworkProp,
|
||||
prefill: unusedFrameworkProp,
|
||||
persistDraft: unusedFrameworkProp,
|
||||
addAttachments: unusedFrameworkProp,
|
||||
removeAttachment: unusedFrameworkProp,
|
||||
|
||||
@@ -29,8 +29,7 @@ const standard: GlobalStandardProps & SessionStandardProps = {
|
||||
useSessionRetainInfo: unused, useResource: unused, useWorkspaces: unused,
|
||||
...allowAllAccess,
|
||||
useContextMenu: contextMenuHook(),
|
||||
inputActions: { insertQuote: () => false, captureInsertion: unused, insertText: unused,
|
||||
setDraft: unused, prefill: unused, persistDraft: unused,
|
||||
inputActions: { insertQuote: () => false, captureInsertion: unused, insertText: unused, setDraft: unused, persistDraft: unused,
|
||||
addAttachments: unused, removeAttachment: unused, pruneAttachments: unused, submit: unused },
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,7 @@ const standard: GlobalStandardProps & SessionStandardProps = {
|
||||
useSessionRetainInfo: unused, useResource: unused, useWorkspaces: unused,
|
||||
...allowAllAccess,
|
||||
useContextMenu: contextMenuHook(),
|
||||
inputActions: { insertQuote: () => false, captureInsertion: unused, insertText: unused,
|
||||
setDraft: unused, prefill: unused, persistDraft: unused,
|
||||
inputActions: { insertQuote: () => false, captureInsertion: unused, insertText: unused, setDraft: unused, persistDraft: unused,
|
||||
addAttachments: unused, removeAttachment: unused, pruneAttachments: unused, submit: unused },
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ const standard: GlobalStandardProps & SessionStandardProps = {
|
||||
useSessionRetainInfo: unused, useResource: unused, useWorkspaces: unused,
|
||||
...allowAllAccess,
|
||||
useContextMenu: contextMenuHook(),
|
||||
inputActions: { insertQuote: () => false, captureInsertion: unused, insertText: unused,
|
||||
setDraft: unused, prefill: unused, persistDraft: unused,
|
||||
inputActions: { insertQuote: () => false, captureInsertion: unused, insertText: unused, setDraft: unused, persistDraft: unused,
|
||||
addAttachments: unused, removeAttachment: unused, pruneAttachments: unused, submit: unused },
|
||||
}
|
||||
const choices: TerminalLaunchShells = {
|
||||
|
||||
@@ -107,7 +107,6 @@ function props(
|
||||
captureInsertion: unused,
|
||||
insertText: unused,
|
||||
setDraft: unused,
|
||||
prefill: unused,
|
||||
persistDraft: unused,
|
||||
addAttachments: unused,
|
||||
removeAttachment: unused,
|
||||
|
||||
@@ -220,7 +220,6 @@ function standaloneProps(
|
||||
captureInsertion: () => ({ start: 0, end: 0, draftRev: 0 }),
|
||||
insertText: () => false,
|
||||
setDraft: () => {},
|
||||
prefill: () => false,
|
||||
persistDraft: () => {},
|
||||
addAttachments: () => false,
|
||||
removeAttachment: () => {},
|
||||
@@ -359,7 +358,6 @@ function mount(fixture: Awaited<ReturnType<typeof bench>>) {
|
||||
captureInsertion: () => ({ start: 0, end: 0, draftRev: 0 }),
|
||||
insertText: () => false,
|
||||
setDraft: vi.fn(),
|
||||
prefill: vi.fn(() => false),
|
||||
persistDraft: vi.fn(),
|
||||
addAttachments: vi.fn(() => false),
|
||||
removeAttachment: vi.fn(),
|
||||
|
||||
@@ -137,7 +137,6 @@ const kit: Omit<QuestionComposerProps, 'matched'> = {
|
||||
captureInsertion: () => ({ start: 0, end: 0, draftRev: 0 }),
|
||||
insertText: () => false,
|
||||
setDraft: () => { throw new Error('unused') },
|
||||
prefill: () => false,
|
||||
persistDraft: () => { throw new Error('unused') },
|
||||
addAttachments: () => { throw new Error('unused') },
|
||||
removeAttachment: () => { throw new Error('unused') },
|
||||
|
||||
@@ -143,7 +143,6 @@ const kitBase: Omit<QuestionComposerProps, 'matched' | 'useStore' | 'useQuestion
|
||||
captureInsertion: () => ({ start: 0, end: 0, draftRev: 0 }),
|
||||
insertText: () => false,
|
||||
setDraft: () => { throw new Error('unused') },
|
||||
prefill: () => false,
|
||||
persistDraft: () => { throw new Error('unused') },
|
||||
addAttachments: () => { throw new Error('unused') },
|
||||
removeAttachment: () => { throw new Error('unused') },
|
||||
|
||||
@@ -355,7 +355,6 @@ function panelProps(data: WorkflowRunChatData, sessions = listState(), openSessi
|
||||
captureInsertion: () => ({ start: 0, end: 0, draftRev: 0 }),
|
||||
insertText: () => false,
|
||||
setDraft: () => {},
|
||||
prefill: () => false,
|
||||
persistDraft: () => {},
|
||||
addAttachments: () => false,
|
||||
removeAttachment: () => {},
|
||||
|
||||
@@ -24,7 +24,6 @@ function fixture(recording?: Recording, navigable = true) {
|
||||
const inputActions = { notify: vi.fn(), insertQuote: () => false,
|
||||
captureInsertion: vi.fn(() => ({ start: 3, end: 3, draftRev: 1 })), insertText: vi.fn(() => true),
|
||||
setDraft: vi.fn(), persistDraft: vi.fn(), addAttachments: vi.fn(() => true), removeAttachment: vi.fn(),
|
||||
prefill: vi.fn(() => false),
|
||||
pruneAttachments: vi.fn(), submit: vi.fn() }
|
||||
const readiness = createSnapshotStore<SpeechReadiness>({ connected: true, error: null, catalog: {
|
||||
providers: [{ id, name: 'SenseVoiceSmall', location: 'host-local', languages: ['auto', 'zh', 'en', 'ja'], preparation: { phase: 'ready' } }],
|
||||
|
||||
@@ -182,12 +182,11 @@ describe('Node program process', () => {
|
||||
})
|
||||
|
||||
it.each([{}, { timeoutMs: null }] as const)('cancels a live program and closes its managed process with %j', async (timing) => {
|
||||
const { run, cwd } = await setup()
|
||||
const { run } = await setup()
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const controller = new AbortController()
|
||||
// Synchronous file readiness does not depend on the busy child's control pipe draining.
|
||||
const active = run({ ...timing, program: 'const fs = await import("node:fs"); fs.writeFileSync("entered", "ready"); for (;;) {}', signal: controller.signal, bindings: [] })
|
||||
onTestFinished(async () => { controller.abort('test finished'); await active })
|
||||
await expect.poll(() => readFile(join(cwd, 'entered'), 'utf8')).toBe('ready')
|
||||
const active = run({ ...timing, program: 'void tools.enter({}); for (;;) {}', signal: controller.signal, bindings: bindings({ enter: async () => { entered.resolve(undefined); return null } }) })
|
||||
await entered.promise
|
||||
controller.abort('stop')
|
||||
expect((await active).error).toEqual({ kind: 'abort', message: 'stop' })
|
||||
})
|
||||
|
||||
@@ -27,8 +27,7 @@ const standard: GlobalStandardProps & SessionStandardProps = {
|
||||
useSessionAccess: () => true, useRemoteAccess: () => true, useWorkspaceAccess: () => true,
|
||||
useConversation: unused, useInput: unused, useChat: unused, useTrajectory: unused,
|
||||
usePanelInfo: unused, useContextMenu: contextMenuHook(), useResource: unused, useWorkspaces: unused,
|
||||
inputActions: { insertQuote: () => false, captureInsertion: unused, insertText: unused,
|
||||
setDraft: unused, prefill: unused, persistDraft: unused,
|
||||
inputActions: { insertQuote: () => false, captureInsertion: unused, insertText: unused, setDraft: unused, persistDraft: unused,
|
||||
addAttachments: unused, removeAttachment: unused, pruneAttachments: unused, submit: unused },
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user