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:
@@ -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
@@ -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 快捷操作',
|
||||
|
||||
@@ -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%;
|
||||
|
||||
Reference in New Issue
Block a user