feat: add model-aware DSH image input

DeepSeek Harness previously treated every selected model as text-only. It now carries the model profile's image capability through Main, the Utility Host, ACP, and Pi-AI, rejects unsupported images before model invocation, and validates supported JPEG/PNG data in a bounded process-local attachment store.

The Composer keeps universal controls on its first row, places OpenCode and Continue controls on a dedicated responsive row, and anchors context compaction without shifting the footer. Runtime supervision ownership for future Subagents and Jobs is documented for the right sidebar.

Release note: DeepSeek Harness 现可按所选模型连接的能力安全接收 JPEG/PNG 图片;Composer 同时将 Runtime 专属选择器移到独立一行,并固定上下文压缩入口。
This commit is contained in:
mesalogo
2026-08-16 23:30:54 +08:00
parent 18263a6b23
commit b751c70d75
30 changed files with 1589 additions and 187 deletions
+35 -4
View File
@@ -886,6 +886,9 @@ describe('App', () => {
expect(await screen.findByText('桌面工作区')).toBeInTheDocument()
expect(screen.getByText('GOODBUDDY 工作台')).toBeInTheDocument()
expect(
document.querySelector('.composer__runtime-toolbar')
).not.toBeInTheDocument()
})
it('schedules lazy workspace routes for idle preloading', () => {
@@ -4555,6 +4558,18 @@ describe('App', () => {
const agentPicker = await screen.findByRole('button', {
name: /OpenCode Runtime Agent/u
})
const runtimeToolbar = screen.getByRole('group', {
name: 'OpenCode 专属功能'
})
const universalSettings = screen.getByRole('group', {
name: '对话设置'
})
expect(runtimeToolbar).toHaveClass('composer__runtime-toolbar')
expect(runtimeToolbar).toContainElement(agentPicker)
expect(universalSettings).not.toContainElement(agentPicker)
expect(universalSettings.closest('.composer__toolbar')).toHaveClass(
'composer__toolbar--with-runtime-controls'
)
fireEvent.click(agentPicker)
fireEvent.click(
within(
@@ -4566,6 +4581,7 @@ describe('App', () => {
const actionPicker = screen.getByRole('button', {
name: /Runtime /u
})
expect(runtimeToolbar).toContainElement(actionPicker)
fireEvent.click(actionPicker)
fireEvent.click(
within(
@@ -4673,6 +4689,14 @@ describe('App', () => {
const presetPicker = await screen.findByRole('button', {
name: /Continue .*使/u
})
const runtimeToolbar = screen.getByRole('group', {
name: 'Continue 专属功能'
})
expect(runtimeToolbar).toHaveClass('composer__runtime-toolbar')
expect(runtimeToolbar).toContainElement(presetPicker)
expect(
screen.getByRole('group', { name: '对话设置' })
).not.toContainElement(presetPicker)
fireEvent.click(presetPicker)
fireEvent.click(
within(
@@ -4684,6 +4708,7 @@ describe('App', () => {
const actionPicker = screen.getByRole('button', {
name: /Runtime /u
})
expect(runtimeToolbar).toContainElement(actionPicker)
fireEvent.click(actionPicker)
fireEvent.click(
within(
@@ -4801,11 +4826,17 @@ describe('App', () => {
render(<App />)
fireEvent.click(
await screen.findByRole('button', {
name: '压缩上下文'
})
const compactContext = await screen.findByRole('button', {
name: '压缩上下文'
})
expect(compactContext.parentElement).toHaveClass(
'composer-meta',
'composer-meta--with-context-compact'
)
expect(
compactContext.parentElement?.firstElementChild
).toBe(compactContext)
fireEvent.click(compactContext)
await waitFor(() =>
expect(api.agent.compactConversation).toHaveBeenCalledWith({
requestId: expect.any(String),
+143 -103
View File
@@ -6159,6 +6159,36 @@ function App(): React.JSX.Element {
activeConversation?.messages.some(
(message) => message.state === 'streaming'
) ?? false
const runtimeAgentControlAvailable =
activeRuntimeSelection?.provider === 'opencode' &&
runtimeAgentOptions.length > 1
const runtimePresetControlAvailable =
activeRuntimeSelection?.provider === 'continue' &&
runtimePresetOptions.length > 1
const runtimeActionControlAvailable =
(activeRuntimeSelection?.provider === 'opencode' ||
activeRuntimeSelection?.provider === 'continue') &&
runtimeActionOptions.length > 1
const runtimeControlsAvailable =
runtimeAgentControlAvailable ||
runtimePresetControlAvailable ||
runtimeActionControlAvailable
const runtimeControlsProvider = runtimeControlsAvailable
? activeRuntimeSelection?.provider === 'opencode'
? 'OpenCode'
: activeRuntimeSelection?.provider === 'continue'
? 'Continue'
: undefined
: undefined
const runtimeControlsLabel = runtimeControlsProvider
? t('composer.runtimeControls.groupLabel', {
runtime: runtimeControlsProvider
})
: ''
const runtimeContextCompactAvailable =
(activeRuntimeSelection?.provider === 'opencode' ||
activeRuntimeSelection?.provider === 'continue') &&
runtimeNativeSnapshot?.context.manualCompact === true
const composerContextMetrics = useMemo(() => {
if (
@@ -7027,7 +7057,13 @@ function App(): React.JSX.Element {
}}
/>
</div>
<div className="composer__toolbar">
<div
className={`composer__toolbar${
runtimeControlsProvider
? ' composer__toolbar--with-runtime-controls'
: ''
}`}
>
<div className="composer__controls">
<div
aria-label={t('composer.addContent')}
@@ -7190,83 +7226,6 @@ function App(): React.JSX.Element {
options={assistantExpertOptions}
value={selectedExpertId}
/>
{activeRuntimeSelection?.provider === 'opencode' &&
runtimeAgentOptions.length > 1 && (
<ComposerMenuSelect
ariaLabel={t(
'composer.runtimeControls.agentLabel'
)}
className="composer-picker--runtime"
disabled={isRunning}
icon={
<TerminalSquare
aria-hidden="true"
size={15}
/>
}
menuOpen={
composerMenuOpen === 'runtime-agent'
}
onChange={setSelectedRuntimeAgent}
onOpenChange={setRuntimeAgentMenuOpen}
options={runtimeAgentOptions}
value={selectedRuntimeAgent}
/>
)}
{activeRuntimeSelection?.provider === 'continue' &&
runtimePresetOptions.length > 1 && (
<ComposerMenuSelect
ariaLabel={t(
'composer.runtimeControls.presetLabel'
)}
className="composer-picker--runtime"
disabled={isRunning}
icon={
<TerminalSquare
aria-hidden="true"
size={15}
/>
}
menuOpen={
composerMenuOpen === 'runtime-preset'
}
onChange={setSelectedContinuePreset}
onOpenChange={setRuntimePresetMenuOpen}
options={runtimePresetOptions}
value={selectedContinuePreset}
/>
)}
{(activeRuntimeSelection?.provider === 'opencode' ||
activeRuntimeSelection?.provider === 'continue') &&
runtimeActionOptions.length > 1 && (
<ComposerMenuSelect
ariaLabel={t(
'composer.runtimeControls.actionLabel'
)}
className="composer-picker--runtime-action"
disabled={isRunning}
icon={
<TerminalSquare
aria-hidden="true"
size={15}
/>
}
menuOpen={
composerMenuOpen === 'runtime-action'
}
onChange={selectRuntimeAction}
onOpenChange={setRuntimeActionMenuOpen}
options={runtimeActionOptions}
value={
runtimeActionOptions.find(
(option) =>
option.action?.type === 'command' &&
option.action.id ===
selectedRuntimeCommand
)?.value ?? ''
}
/>
)}
<ComposerMenuSelect
ariaLabel={t('composer.modeLabel')}
className={`composer-picker--mode composer-picker--${effectiveWorkMode}`}
@@ -7575,8 +7534,113 @@ function App(): React.JSX.Element {
</button>
)}
</div>
{runtimeControlsProvider && (
<div
aria-label={runtimeControlsLabel}
className="composer__runtime-toolbar"
role="group"
>
<strong className="composer__runtime-toolbar-label">
{runtimeControlsLabel}
</strong>
<div className="composer__runtime-controls">
{runtimeAgentControlAvailable && (
<ComposerMenuSelect
ariaLabel={t(
'composer.runtimeControls.agentLabel'
)}
className="composer-picker--runtime"
disabled={isRunning}
icon={
<TerminalSquare aria-hidden="true" size={15} />
}
menuOpen={
composerMenuOpen === 'runtime-agent'
}
onChange={setSelectedRuntimeAgent}
onOpenChange={setRuntimeAgentMenuOpen}
options={runtimeAgentOptions}
value={selectedRuntimeAgent}
/>
)}
{runtimePresetControlAvailable && (
<ComposerMenuSelect
ariaLabel={t(
'composer.runtimeControls.presetLabel'
)}
className="composer-picker--runtime"
disabled={isRunning}
icon={
<TerminalSquare aria-hidden="true" size={15} />
}
menuOpen={
composerMenuOpen === 'runtime-preset'
}
onChange={setSelectedContinuePreset}
onOpenChange={setRuntimePresetMenuOpen}
options={runtimePresetOptions}
value={selectedContinuePreset}
/>
)}
{runtimeActionControlAvailable && (
<ComposerMenuSelect
ariaLabel={t(
'composer.runtimeControls.actionLabel'
)}
className="composer-picker--runtime-action"
disabled={isRunning}
icon={
<TerminalSquare aria-hidden="true" size={15} />
}
menuOpen={
composerMenuOpen === 'runtime-action'
}
onChange={selectRuntimeAction}
onOpenChange={setRuntimeActionMenuOpen}
options={runtimeActionOptions}
value={
runtimeActionOptions.find(
(option) =>
option.action?.type === 'command' &&
option.action.id ===
selectedRuntimeCommand
)?.value ?? ''
}
/>
)}
</div>
</div>
)}
</div>
<div className="composer-meta">
<div
className={`composer-meta${
runtimeContextCompactAvailable
? ' composer-meta--with-context-compact'
: ''
}`}
>
{runtimeContextCompactAvailable && (
<button
className="composer-context-compact"
disabled={runtimeContextCompacting || isRunning}
onClick={() => void compactRuntimeContext()}
title={runtimeNativeSnapshot?.context.detail}
type="button"
>
{runtimeContextCompacting ? (
<LoaderCircle
aria-hidden="true"
className="context-chip__spinner"
size={13}
/>
) : (
<RefreshCw aria-hidden="true" size={13} />
)}
{runtimeContextCompacting
? t('composer.context.compacting')
: t('composer.context.compact')}
</button>
)}
{composerContextMetrics && (
<div
className={`composer-context-meter${
@@ -7681,30 +7745,6 @@ function App(): React.JSX.Element {
)}
</div>
)}
{(activeRuntimeSelection?.provider === 'opencode' ||
activeRuntimeSelection?.provider === 'continue') &&
runtimeNativeSnapshot?.context.manualCompact && (
<button
className="composer-context-compact"
disabled={runtimeContextCompacting || isRunning}
onClick={() => void compactRuntimeContext()}
title={runtimeNativeSnapshot.context.detail}
type="button"
>
{runtimeContextCompacting ? (
<LoaderCircle
aria-hidden="true"
className="context-chip__spinner"
size={13}
/>
) : (
<RefreshCw aria-hidden="true" size={13} />
)}
{runtimeContextCompacting
? t('composer.context.compacting')
: t('composer.context.compact')}
</button>
)}
{contextError && (
<span className="composer-meta__error">
{contextError}
@@ -119,6 +119,39 @@ describe('WorkspacePrimitives', () => {
)
})
it('floats Runtime context compaction without shifting the composer', () => {
expect(stylesheet).toMatch(
/\.composer-wrap\s*\{[^}]*var\(--space-2\);[^}]*background:\s*var\(--surface-raised\);/u
)
expect(stylesheet).toMatch(
/\.composer-meta\s*\{[^}]*position:\s*relative;[^}]*min-height:\s*26px;[^}]*margin:\s*var\(--space-1\) 0 0;/u
)
expect(stylesheet).toMatch(
/\.composer-meta--with-context-compact\s*\{[^}]*padding-left:\s*calc\(/u
)
expect(stylesheet).toMatch(
/\.composer-context-compact\s*\{[^}]*position:\s*absolute;[^}]*bottom:\s*0;[^}]*left:\s*0;/u
)
})
it('separates Runtime-specific controls from the main composer toolbar', () => {
expect(stylesheet).toMatch(
/\.composer__toolbar--with-runtime-controls\s*\{[^}]*border-radius:\s*0;/u
)
expect(stylesheet).toMatch(
/\.composer__runtime-toolbar\s*\{[^}]*display:\s*flex;[^}]*min-height:\s*52px;[^}]*border-top:\s*1px solid var\(--border-default\);[^}]*background:\s*var\(--surface-muted\);/u
)
expect(stylesheet).toMatch(
/\.composer__runtime-controls\s*\{[^}]*display:\s*flex;[^}]*flex-wrap:\s*wrap;/u
)
expect(stylesheet).toMatch(
/\.composer-picker--runtime > \.model-button,\s*\.composer-picker--runtime-action > \.model-button\s*\{[^}]*width:\s*220px;/u
)
expect(stylesheet).toMatch(
/\.composer__runtime-controls \.composer-picker\s*\{[^}]*flex-basis:\s*220px;/u
)
})
it('keeps shared controls keyboard and pointer accessible at narrow widths', () => {
expect(stylesheet).toMatch(
/button\s*>\s*svg,\s*button\s*>\s*svg\s+\*\s*\{[^}]*pointer-events:\s*none;/u
@@ -322,6 +322,7 @@ export const app = {
expertLabel: 'Expert role',
modeLabel: 'Work mode',
runtimeControls: {
groupLabel: '{{runtime}} controls',
agentLabel: 'OpenCode Runtime Agent',
presetLabel: 'Continue configuration preset',
actionLabel: 'Runtime shortcut',
@@ -314,6 +314,7 @@ export const app = {
expertLabel: '专家角色',
modeLabel: '工作模式',
runtimeControls: {
groupLabel: '{{runtime}} 专属功能',
agentLabel: 'OpenCode Runtime Agent',
presetLabel: 'Continue 配置预设',
actionLabel: 'Runtime 快捷操作',
+70 -3
View File
@@ -4038,7 +4038,7 @@ button > svg {
padding:
var(--space-3)
max(var(--page-gutter), calc((100% - var(--content-reading)) / 2))
var(--space-4);
var(--space-2);
background: var(--surface-raised);
}
@@ -4387,6 +4387,42 @@ button > svg {
gap: var(--space-3);
}
.composer__toolbar--with-runtime-controls {
border-radius: 0;
}
.composer__runtime-toolbar {
display: flex;
min-width: 0;
min-height: 52px;
padding: var(--space-2) var(--space-3);
align-items: center;
border-top: 1px solid var(--border-default);
border-radius: 0 0 calc(var(--radius-card) - 1px)
calc(var(--radius-card) - 1px);
background: var(--surface-muted);
gap: var(--space-3);
}
.composer__runtime-toolbar-label {
min-width: 0;
padding-right: var(--space-3);
border-right: 1px solid var(--border-default);
color: var(--text-secondary);
font-size: var(--font-caption);
font-weight: 650;
white-space: nowrap;
}
.composer__runtime-controls {
display: flex;
min-width: 0;
flex: 1;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
}
.composer__controls {
display: flex;
min-width: 0;
@@ -4483,7 +4519,7 @@ button > svg {
.composer-picker--runtime > .model-button,
.composer-picker--runtime-action > .model-button {
width: 138px;
width: 220px;
}
.composer-picker--ask svg {
@@ -4670,8 +4706,12 @@ button > svg {
}
.composer-meta {
--composer-context-compact-width: 124px;
position: relative;
display: flex;
margin: var(--space-2) 0 0;
min-height: 26px;
margin: var(--space-1) 0 0;
align-items: center;
justify-content: flex-end;
font-size: var(--font-caption);
@@ -4680,6 +4720,12 @@ button > svg {
line-height: 1.45;
}
.composer-meta--with-context-compact {
padding-left: calc(
var(--composer-context-compact-width) + var(--space-3)
);
}
.composer-meta__error {
margin-right: auto;
}
@@ -4694,12 +4740,17 @@ button > svg {
}
.composer-context-compact {
position: absolute;
bottom: 0;
left: 0;
display: inline-flex;
width: var(--composer-context-compact-width);
min-height: 26px;
padding: var(--space-1) var(--space-2);
border: 1px solid var(--border-control);
border-radius: var(--radius-control);
align-items: center;
justify-content: center;
background: var(--surface-raised);
color: var(--text-secondary);
font-size: var(--font-caption);
@@ -4786,6 +4837,22 @@ button > svg {
flex: 1 1 150px;
}
.composer__runtime-toolbar {
align-items: stretch;
flex-direction: column;
}
.composer__runtime-toolbar-label {
padding-right: 0;
padding-bottom: var(--space-2);
border-right: 0;
border-bottom: 1px solid var(--border-default);
}
.composer__runtime-controls .composer-picker {
flex-basis: 220px;
}
.composer-picker > .model-button,
.model-button {
width: 100%;