mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
polish(ui): apply ui-skills audit across a11y, motion cost, and numerics
fixing-accessibility: expandable rows expose aria-expanded/controls, user messages become keyboard-operable buttons, pane dividers and the composer gain accessible names, the live status row announces politely, the busy form sets aria-busy, feedback failures render inline next to the form, and the muted/faint text tiers rise to AA-viable contrast. fixing-motion-performance: composer autosize moves to CSS field-sizing (dropping the per-keystroke measure/write cycle) and streaming scroll writes coalesce per frame while direct user actions stay synchronous. baseline-ui: data columns use tabular numerals, headings balance and prose wraps pretty, and z-index joins the token scale. improve-ui: straggler radii and control heights land on the declared size scale (radius-xs joins the ramp). fixing-metadata: the document language follows the active locale.
This commit is contained in:
@@ -211,7 +211,7 @@ function buildShell(): void {
|
||||
<aside class="source-list">
|
||||
<header class="app-brand">
|
||||
<button class="brand-title" data-module="sessions">DeepSeek Harness</button>
|
||||
<span class="runtime-dot starting" id="runtimeDot"></span>
|
||||
<span class="runtime-dot starting" id="runtimeDot" aria-hidden="true"></span>
|
||||
</header>
|
||||
<section class="primary-actions">
|
||||
<button class="primary-action" data-action="new-session"><span data-text="app.newChat"></span><kbd>⌘N</kbd></button>
|
||||
@@ -233,7 +233,7 @@ function buildShell(): void {
|
||||
<strong id="footerRuntime"></strong>
|
||||
</footer>
|
||||
</aside>
|
||||
<div class="pane-divider" id="dividerLeft" role="separator" aria-orientation="vertical"></div>
|
||||
<div class="pane-divider" id="dividerLeft" role="separator" aria-orientation="vertical" data-label="app.resizeSidebar"></div>
|
||||
<main class="harness-main">
|
||||
<header class="topbar">
|
||||
<div class="title-area"><h1 id="topbarTitle"></h1></div>
|
||||
@@ -283,7 +283,7 @@ function buildShell(): void {
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
<div class="pane-divider" id="dividerInspector" role="separator" aria-orientation="vertical" hidden></div>
|
||||
<div class="pane-divider" id="dividerInspector" role="separator" aria-orientation="vertical" hidden data-label="app.resizeInspector"></div>
|
||||
<aside class="inspector" id="inspector" hidden>
|
||||
<header class="inspector-head">
|
||||
<div>
|
||||
@@ -348,11 +348,16 @@ function applyStaticText(): void {
|
||||
for (const node of appEl.querySelectorAll<HTMLElement>('[data-title]')) {
|
||||
node.title = t(node.dataset.title as I18nKey)
|
||||
}
|
||||
for (const node of appEl.querySelectorAll<HTMLElement>('[data-label]')) {
|
||||
node.setAttribute('aria-label', t(node.dataset.label as I18nKey))
|
||||
}
|
||||
document.documentElement.lang = state.locale
|
||||
el.searchInput.placeholder = t('app.searchPlaceholder')
|
||||
el.composerInput.placeholder = state.selectedSessionId === undefined
|
||||
? t('composer.placeholderDraft')
|
||||
: t('composer.placeholderSession')
|
||||
el.sendButton.setAttribute('aria-label', t('composer.send'))
|
||||
el.composerInput.setAttribute('aria-label', t('composer.send'))
|
||||
updateComposerState()
|
||||
}
|
||||
|
||||
@@ -652,7 +657,7 @@ function ensureLiveSkeleton(live: LiveTurn): void {
|
||||
<div data-live="tools"></div>
|
||||
</div>
|
||||
<div class="assistant-prose" data-live="answer" hidden></div>
|
||||
<div class="live-status" data-live="status">${escapeHtml(t('chat.sending'))}</div>
|
||||
<div class="live-status" role="status" data-live="status">${escapeHtml(t('chat.sending'))}</div>
|
||||
</div>
|
||||
</article>
|
||||
`
|
||||
@@ -670,11 +675,26 @@ function ensureLiveSkeleton(live: LiveTurn): void {
|
||||
}
|
||||
}
|
||||
|
||||
let scrollToBottomQueued = false
|
||||
|
||||
/** Direct user actions scroll immediately; streaming updates coalesce per frame. */
|
||||
function scrollChatToBottom(force = false): void {
|
||||
if (!force && !state.stickToBottom) return
|
||||
el.chatView.scrollTop = el.chatView.scrollHeight
|
||||
state.stickToBottom = true
|
||||
updateLiveJump()
|
||||
if (force) {
|
||||
scrollToBottomQueued = false
|
||||
el.chatView.scrollTop = el.chatView.scrollHeight
|
||||
state.stickToBottom = true
|
||||
updateLiveJump()
|
||||
return
|
||||
}
|
||||
if (!state.stickToBottom || scrollToBottomQueued) return
|
||||
scrollToBottomQueued = true
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!scrollToBottomQueued) return
|
||||
scrollToBottomQueued = false
|
||||
el.chatView.scrollTop = el.chatView.scrollHeight
|
||||
state.stickToBottom = true
|
||||
updateLiveJump()
|
||||
})
|
||||
}
|
||||
|
||||
function updateLiveJump(): void {
|
||||
@@ -690,11 +710,7 @@ function updateComposerState(): void {
|
||||
el.cancelButton.hidden = !busy
|
||||
el.composerHint.textContent = busy ? t('chat.working') : t('composer.hint')
|
||||
el.composerForm.classList.toggle('busy', busy)
|
||||
}
|
||||
|
||||
function autosizeComposer(): void {
|
||||
el.composerInput.style.height = '0px'
|
||||
el.composerInput.style.height = `${Math.min(132, Math.max(42, el.composerInput.scrollHeight))}px`
|
||||
el.composerForm.setAttribute('aria-busy', String(busy))
|
||||
}
|
||||
|
||||
async function sendPrompt(prompt: string): Promise<void> {
|
||||
@@ -714,7 +730,6 @@ async function sendPrompt(prompt: string): Promise<void> {
|
||||
})
|
||||
state.busySessionId = draftKey
|
||||
el.composerInput.value = ''
|
||||
autosizeComposer()
|
||||
updateComposerState()
|
||||
showError('')
|
||||
renderLiveTurn()
|
||||
@@ -762,7 +777,6 @@ async function sendPrompt(prompt: string): Promise<void> {
|
||||
showError(String(error))
|
||||
if ((state.selectedSessionId === sessionId || (sessionId === undefined && state.draftChat)) && el.composerInput.value.trim().length === 0) {
|
||||
el.composerInput.value = prompt
|
||||
autosizeComposer()
|
||||
}
|
||||
updateComposerState()
|
||||
} finally {
|
||||
@@ -907,7 +921,7 @@ function renderConversation(): void {
|
||||
function renderConversationTurn(turn: ChatTurn): string {
|
||||
const userTarget = turn.userTargetId === undefined ? undefined : state.graph.targets.get(turn.userTargetId)
|
||||
const user = userTarget === undefined ? '' : `
|
||||
<article class="message user ${selectedTargetClass(userTarget.id)}" data-target-id="${escapeHtml(userTarget.id)}">
|
||||
<article class="message user ${selectedTargetClass(userTarget.id)}" role="button" tabindex="0" data-target-id="${escapeHtml(userTarget.id)}">
|
||||
<div class="message-card"><div class="user-bubble">${escapeHtml(contentText(userTarget.output))}</div></div>
|
||||
</article>
|
||||
`
|
||||
@@ -932,9 +946,9 @@ function renderConversationActivity(activity: ChatActivity): string {
|
||||
<section class="chat-activity thinking ${selectedTargetClass(target.id)}">
|
||||
<div class="activity-row">
|
||||
<button class="activity-select" type="button" data-target-id="${escapeHtml(target.id)}"><span>${escapeHtml(t('chat.thinking'))}</span><strong>${escapeHtml(truncate(contentText(target.output), 112))}</strong></button>
|
||||
<button class="activity-toggle" type="button" data-toggle-activity="${escapeHtml(target.id)}" aria-label="${escapeHtml(t(expanded ? 'trace.collapseRow' : 'trace.expandRow'))}">${expanded ? '⌃' : '⌄'}</button>
|
||||
<button class="activity-toggle" type="button" data-toggle-activity="${escapeHtml(target.id)}" aria-expanded="${expanded}" aria-controls="act-${escapeHtml(target.id)}" aria-label="${escapeHtml(t(expanded ? 'trace.collapseRow' : 'trace.expandRow'))}">${expanded ? '⌃' : '⌄'}</button>
|
||||
</div>
|
||||
<div class="activity-body" ${expanded ? '' : 'hidden'} data-target-id="${escapeHtml(target.id)}"><div>${escapeHtml(contentText(target.output))}</div></div>
|
||||
<div class="activity-body" id="act-${escapeHtml(target.id)}" ${expanded ? '' : 'hidden'} data-target-id="${escapeHtml(target.id)}"><div>${escapeHtml(contentText(target.output))}</div></div>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
@@ -951,9 +965,9 @@ function renderChatToolActivity(target: TraceTarget): string {
|
||||
<section class="chat-activity tool-use ${failed ? 'failed' : ''} ${selectedTargetClass(target.id)}">
|
||||
<div class="activity-row">
|
||||
<button class="activity-select" type="button" data-target-id="${escapeHtml(target.id)}"><span>${escapeHtml(failed ? t('chat.toolFailed') : t('chat.toolUse'))}</span><strong>${escapeHtml(target.title)}${preview.length > 0 ? `<span class="activity-preview"> · ${escapeHtml(preview)}</span>` : ''}</strong></button>
|
||||
<button class="activity-toggle" type="button" data-toggle-activity="${escapeHtml(target.id)}" aria-label="${escapeHtml(t(expanded ? 'trace.collapseRow' : 'trace.expandRow'))}">${expanded ? '⌃' : '⌄'}</button>
|
||||
<button class="activity-toggle" type="button" data-toggle-activity="${escapeHtml(target.id)}" aria-expanded="${expanded}" aria-controls="act-${escapeHtml(target.id)}" aria-label="${escapeHtml(t(expanded ? 'trace.collapseRow' : 'trace.expandRow'))}">${expanded ? '⌃' : '⌄'}</button>
|
||||
</div>
|
||||
<div class="activity-body" ${expanded ? '' : 'hidden'} data-target-id="${escapeHtml(target.id)}">${inputHtml}${outputHtml}${spawnedHtml}</div>
|
||||
<div class="activity-body" id="act-${escapeHtml(target.id)}" ${expanded ? '' : 'hidden'} data-target-id="${escapeHtml(target.id)}">${inputHtml}${outputHtml}${spawnedHtml}</div>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
@@ -1055,7 +1069,7 @@ function renderGraphTrajectoryRow(targetId: string): string {
|
||||
<span class="token-cell">${escapeHtml(usage.output)}</span>
|
||||
<span class="token-cell">${escapeHtml(usage.think)}</span>
|
||||
<span class="token-cell offset">+${escapeHtml(formatMs(target.startTime - state.graph.startTime))}</span>
|
||||
<button class="chevron traj-expand" type="button" data-toggle-traj="${escapeHtml(target.id)}" aria-label="${escapeHtml(t(expanded ? 'trace.collapseRow' : 'trace.expandRow'))}">${expanded ? '▾' : '▸'}</button>
|
||||
<button class="chevron traj-expand" type="button" data-toggle-traj="${escapeHtml(target.id)}" aria-expanded="${expanded}" aria-label="${escapeHtml(t(expanded ? 'trace.collapseRow' : 'trace.expandRow'))}">${expanded ? '▾' : '▸'}</button>
|
||||
</div>
|
||||
${expanded ? renderGraphTrajectoryBody(target) : ''}
|
||||
</article>
|
||||
@@ -1463,10 +1477,19 @@ async function submitFeedback(form: HTMLFormElement, sessionId: string, targetId
|
||||
state.trace.feedback = [...(state.trace.feedback ?? []), record]
|
||||
}
|
||||
state.feedbackDrafts.delete(feedbackDraftKey(sessionId, targetId))
|
||||
form.querySelector('.form-error')?.remove()
|
||||
toast(t('feedback.saved'))
|
||||
return true
|
||||
} catch (error) {
|
||||
toast(`${t('feedback.failed')}: ${String(error)}`)
|
||||
// The error belongs next to the form, not only in a transient toast.
|
||||
let errorLine = form.querySelector<HTMLElement>('.form-error')
|
||||
if (errorLine === null) {
|
||||
errorLine = document.createElement('p')
|
||||
errorLine.className = 'form-error'
|
||||
errorLine.setAttribute('role', 'alert')
|
||||
form.appendChild(errorLine)
|
||||
}
|
||||
errorLine.textContent = `${t('feedback.failed')}: ${String(error)}`
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1490,7 +1513,6 @@ function wireStaticEvents(): void {
|
||||
})
|
||||
|
||||
el.composerInput.addEventListener('input', () => {
|
||||
autosizeComposer()
|
||||
updateComposerState()
|
||||
})
|
||||
el.composerInput.addEventListener('keydown', (event) => {
|
||||
|
||||
@@ -75,6 +75,8 @@ const messages = {
|
||||
'toast.copied': '已复制',
|
||||
'feedback.saved': '标注已保存',
|
||||
'feedback.failed': '标注保存失败',
|
||||
'app.resizeSidebar': '调整侧栏宽度',
|
||||
'app.resizeInspector': '调整检查器宽度',
|
||||
'kind.user': '用户',
|
||||
'kind.reasoning': '思考',
|
||||
'kind.assistant': '回复',
|
||||
@@ -346,6 +348,8 @@ const messages = {
|
||||
'toast.copied': 'Copied',
|
||||
'feedback.saved': 'Feedback saved',
|
||||
'feedback.failed': 'Failed to save feedback',
|
||||
'app.resizeSidebar': 'Resize sidebar',
|
||||
'app.resizeInspector': 'Resize inspector',
|
||||
'kind.user': 'User',
|
||||
'kind.reasoning': 'Thinking',
|
||||
'kind.assistant': 'Response',
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
--fg: #20201f;
|
||||
--ink: var(--fg);
|
||||
--ink-soft: color-mix(in oklab, var(--fg) 72%, transparent);
|
||||
--muted: color-mix(in oklab, var(--fg) 52%, transparent);
|
||||
--faint: color-mix(in oklab, var(--fg) 36%, transparent);
|
||||
--muted: color-mix(in oklab, var(--fg) 58%, transparent);
|
||||
--faint: color-mix(in oklab, var(--fg) 44%, transparent);
|
||||
|
||||
/* Borders derive from the same foreground. */
|
||||
--line: color-mix(in oklab, var(--fg) 11%, transparent);
|
||||
@@ -49,11 +49,18 @@
|
||||
--control-xl: 36px;
|
||||
--row-table: 30px;
|
||||
--row-list: 42px;
|
||||
--radius-xs: 6px;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
|
||||
/* Z-index scale: sticky headers, floating controls, overlays, toast. */
|
||||
--z-sticky: 2;
|
||||
--z-floating: 3;
|
||||
--z-overlay: 10;
|
||||
--z-toast: 40;
|
||||
|
||||
/* Motion: one curve, three duration tiers. */
|
||||
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
--duration-fast: 80ms;
|
||||
@@ -567,7 +574,7 @@ kbd {
|
||||
margin: 0 auto 18px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 15px;
|
||||
border-radius: var(--radius-xl);
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
@@ -699,7 +706,7 @@ kbd {
|
||||
max-width: min(680px, 76%);
|
||||
padding: 10px 13px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 17px;
|
||||
border-radius: var(--radius-xl);
|
||||
background: #f2f2f0;
|
||||
color: var(--ink);
|
||||
}
|
||||
@@ -785,7 +792,7 @@ kbd {
|
||||
height: var(--control-md);
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-xs);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@@ -984,7 +991,7 @@ pre {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
min-height: var(--row-table);
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius-md);
|
||||
text-align: left;
|
||||
@@ -1243,7 +1250,7 @@ pre {
|
||||
.traj-group-head {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
z-index: var(--z-sticky);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
@@ -1473,7 +1480,7 @@ pre {
|
||||
}
|
||||
|
||||
.traj-inspect {
|
||||
height: 24px;
|
||||
height: var(--control-sm);
|
||||
padding: 0 7px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
@@ -1495,7 +1502,7 @@ pre {
|
||||
overflow: auto;
|
||||
padding: 12px 16px 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -1510,7 +1517,7 @@ pre {
|
||||
.wf-head-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
z-index: var(--z-sticky);
|
||||
margin-bottom: 6px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
@@ -1547,7 +1554,7 @@ pre {
|
||||
width: 22px;
|
||||
height: 17px;
|
||||
place-items: center;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius-xs);
|
||||
background: #eef1f6;
|
||||
color: #4b5565;
|
||||
font-family: "SF Mono", ui-monospace, Menlo, Monaco, Consolas, monospace;
|
||||
@@ -1594,7 +1601,7 @@ pre {
|
||||
.wf-track {
|
||||
position: relative;
|
||||
height: 22px;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius-xs);
|
||||
background-image: linear-gradient(90deg, rgba(135, 145, 160, 0.18) 1px, transparent 1px);
|
||||
background-repeat: repeat-x;
|
||||
background-size: 25% 100%;
|
||||
@@ -1631,7 +1638,7 @@ pre {
|
||||
.module-card,
|
||||
.compare-column {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
@@ -1686,8 +1693,9 @@ pre {
|
||||
}
|
||||
|
||||
.composer textarea {
|
||||
field-sizing: content;
|
||||
min-height: var(--row-list);
|
||||
max-height: 128px;
|
||||
max-height: 132px;
|
||||
resize: none;
|
||||
overflow: auto;
|
||||
border: 0;
|
||||
@@ -2270,7 +2278,7 @@ pre {
|
||||
.dev-loop-card {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
@@ -2350,7 +2358,7 @@ dd {
|
||||
top: 58px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
z-index: var(--z-overlay);
|
||||
width: 360px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
@@ -2399,7 +2407,7 @@ dd {
|
||||
.chat-live-jump {
|
||||
position: sticky;
|
||||
bottom: 12px;
|
||||
z-index: 3;
|
||||
z-index: var(--z-floating);
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
@@ -2754,7 +2762,7 @@ dd {
|
||||
.assistant-prose code,
|
||||
.assistant-text code {
|
||||
padding: 1px 5px;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius-xs);
|
||||
background: rgba(0, 0, 0, 0.055);
|
||||
font-family: "SF Mono", ui-monospace, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.88em;
|
||||
@@ -2826,7 +2834,7 @@ dd {
|
||||
position: fixed;
|
||||
bottom: 26px;
|
||||
left: 50%;
|
||||
z-index: 40;
|
||||
z-index: var(--z-toast);
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
background: var(--ink);
|
||||
@@ -2936,3 +2944,35 @@ dd {
|
||||
.inspector-open .traj-tree {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── ui-skills pass: numerics, inline form errors ─────────────────────────── */
|
||||
|
||||
/* Data columns and metrics read as columns of digits: keep them tabular. */
|
||||
.token-cell,
|
||||
.wf-dur,
|
||||
.g-dur,
|
||||
.t-dur,
|
||||
.wf-summary strong,
|
||||
.session-copy small,
|
||||
.run-metrics strong {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Inline form errors live next to the action, not only in a toast. */
|
||||
.form-error {
|
||||
margin: 6px 0 0;
|
||||
color: var(--red);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Headings balance; prose avoids awkward last lines. */
|
||||
.empty-thread h2,
|
||||
.empty-state h2 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.assistant-prose p,
|
||||
.empty-thread p,
|
||||
.empty-state p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user