fix(ui): pin dock rows, verb labels on live tools, dedupe reject

Auto placement floated the new docks to the top of the main pane; every
dock now owns an explicit grid row (and the layout regression guards
them). Live tool rows use the streamed kind verb like persisted rows,
and the approval card's fallback reject is omitted when the request
already carries a reject option.
This commit is contained in:
NI0317
2026-07-20 10:58:21 +08:00
parent d3ac437616
commit 37a03df733
3 changed files with 28 additions and 7 deletions
+6 -3
View File
@@ -666,7 +666,7 @@ function renderLiveTurn(): void {
const label = row.querySelector<HTMLElement>('summary strong')
if (label !== null) label.textContent = tool.title
const statusEl = row.querySelector<HTMLElement>('summary span')
if (statusEl !== null) statusEl.textContent = tool.status === 'failed' ? t('chat.toolFailed') : t('chat.toolUse')
if (statusEl !== null) statusEl.textContent = tool.status === 'failed' ? t('chat.toolFailed') : verbForKind(state.liveToolMeta.get(tool.callId)?.kind)
const body = row.querySelector<HTMLElement>('.activity-body pre')
if (body !== null) body.textContent = tool.detail
row.classList.toggle('failed', tool.status === 'failed')
@@ -1088,7 +1088,7 @@ function renderInteractionDock(): void {
<div class="interaction-actions">
${request.kind === 'elicitation' ? `<button type="button" class="allow" data-respond-accept="${escapeHtml(request.id)}">${escapeHtml(t('interaction.accept'))}</button>` : ''}
${request.options.map(option => `<button type="button" class="${option.kind.startsWith('allow') ? 'allow' : 'reject'}" data-respond-option="${escapeHtml(option.optionId)}" data-respond-id="${escapeHtml(request.id)}">${escapeHtml(option.name)}</button>`).join('')}
<button type="button" data-respond-cancel="${escapeHtml(request.id)}">${escapeHtml(t('interaction.dismiss'))}</button>
${request.options.some(option => option.kind.startsWith('reject')) ? '' : `<button type="button" data-respond-cancel="${escapeHtml(request.id)}">${escapeHtml(t('interaction.dismiss'))}</button>`}
</div>
</section>
`).join('')
@@ -1170,7 +1170,10 @@ function liveToolTitle(target: TraceTarget): string | undefined {
/** Verb label for a tool row: the streamed ACP kind beats the generic noun. */
function toolVerbLabel(target: TraceTarget): string {
const kind = liveToolMetaOf(target).kind
return verbForKind(liveToolMetaOf(target).kind)
}
function verbForKind(kind: string | undefined): string {
if (kind === 'read') return t('kind.verb.read')
if (kind === 'edit') return t('kind.verb.edit')
if (kind === 'delete') return t('kind.verb.delete')
+15 -2
View File
@@ -391,7 +391,8 @@ kbd {
display: grid;
min-width: 0;
min-height: 0;
grid-template-rows: 58px auto minmax(0, 1fr) auto;
/* topbar / notice / canvas / interaction / queue / plan / composer */
grid-template-rows: 58px auto minmax(0, 1fr) auto auto auto auto;
background: var(--paper);
}
@@ -408,10 +409,22 @@ kbd {
grid-row: 3;
}
.composer {
#interactionDock {
grid-row: 4;
}
#queueDock {
grid-row: 5;
}
#planDock {
grid-row: 6;
}
.composer {
grid-row: 7;
}
.topbar {
display: grid;
grid-template-columns: minmax(220px, 1fr) auto auto;
@@ -41,10 +41,15 @@ describe('desktop live content', () => {
})
describe('desktop shell layout', () => {
it('pins the composer to its intrinsic bottom row', async () => {
it('pins the docks and composer to their explicit bottom rows', async () => {
const css = await readFile(new URL('../src/styles.css', import.meta.url), 'utf8')
expect(css).toMatch(/\.session-canvas,\s*\.module-canvas\s*{\s*grid-row: 3;/)
expect(css).toMatch(/\.composer\s*{\s*grid-row: 4;/)
// Every dock needs an explicit row: auto placement once floated the queue
// to the top of the main pane.
expect(css).toMatch(/#interactionDock\s*{\s*grid-row: 4;/)
expect(css).toMatch(/#queueDock\s*{\s*grid-row: 5;/)
expect(css).toMatch(/#planDock\s*{\s*grid-row: 6;/)
expect(css).toMatch(/\.composer\s*{\s*grid-row: 7;/)
})
it('does not launch Electron after a strict-port Vite failure', async () => {