Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Assistant workspace consistency implementation plan

> **For agentic workers:** Use subagent-driven-development with one implementer and separate spec/quality reviews. Preserve multiple commits as authorized by the owner.

**Goal:** Align AI subpage controls, spacing and navigation using the existing design-system tokens.

**Architecture:** Host stylesheet owns visual policy. A small view-header composition reuses existing heading/button nodes; each source/artifact/settings/panel owner retains callbacks and state. No shared runtime, persistence or provider contract changes.

**Tech stack:** TypeScript DOM views, ranui components/public CSS hooks, Vitest, Playwright.

## Chunk 1: Uniform workspace

- [x] Inspect deployed pages and code; record measured drift, undefined tokens, consumer boundaries and acceptance in the companion spec.
- [x] Add failing browser geometry/state tests for Sources/Models/History/Artifacts, native versus ranui 32 px frames, consistent titles/insets/gaps, enabled hover and disabled no-hover, coarse targets and long labels. Add focused tests for header composition and artifact dirty/busy back paths before implementing behavior changes.
- [x] Consolidate relevant rules in `styles/assistant.css` at their existing owner, replace undefined spacing/hover tokens, use tokenized type/spacing/radius/states, maintain content-sized editors, primary versus ordinary/destructive actions, and touch minima.
- [x] Add `lib/agent-plugin/ui/view-header.ts`; integrate Sources, Artifacts, Models and History owners without moving or duplicating state. Preserve artifact button registry, labels, dirty confirmation, asynchronous generation guards and focus restoration. Add destination label only where existing vocabulary is insufficient, updating all seven locales and types.
- [x] Run focused units and browser regression red/green, inspect screenshots and self-review; commit implementation and tests. Spec review, then quality review; resolve concrete findings and reverify.

## Chunk 2: Acceptance and delivery

- [x] Run full unit coverage, lint/types, format and production build; run relevant UI/source/artifact/native browser workflows separately from timing-sensitive unit workloads.
- [ ] Verify light/dark/narrow/coarse/keyboard/long-label states and actual deployed preview. Save a compact screenshot deliverable.
- [ ] Create the follow-up PR because #276 is merged, attach it, wait for all latest-head checks, fix failures and perform a final incremental review. Record authoritative CI/review results in the PR description; do not merge.

## Local acceptance

- Full coverage: 6,169 tests in 231 files passed; statements 86.77%, branches 82.89%, functions 87.51%, lines 89.33%. Lint, workspace declarations, TypeScript, formatting and production build passed.
- The 11-suite browser run passed 38 cases and exposed one superseded history-title alignment assertion. The revised assertion checks the header/body leading edge and the exact back-to-title gap; the complete long-history case then passed on a fresh build. All 39 targeted scenarios are verified, including native document readback and Undo, IME/dictation, queued requests, offline sources, artifact revisions and draft restoration.
- Independent specification review approved `a0bb0d3`; quality review found a masked ranui danger-color hook. `abb672f` restores that public hook and adds a real-component red/green regression for deletion, confirmation, hover, pressed and pending-disabled states. Quality re-review approved that fix.
- Root inspected Sources, Models, Conversations and Artifacts screenshots in light/dark and narrow layouts. The optional Google OAuth deployment remains unconfigured and is outside this visual increment; no real authorization or model-quality claim is made.
- Deployed-preview acceptance and latest-head remote CI remain pending until the follow-up PR is created.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Assistant workspace consistency

## Scope and evidence

The owner requested uniform buttons, spacing and interactions across the AI workspace, explicitly using design-system tokens. PR #276 is already merged; this increment starts from `origin/main` (`2427b27`) and requires a follow-up PR. Prior approval for compact, flat UI and continued implementation remains in force.

Observed on the deployed preview: Sources commands are 38 px and native fields 40 px; model controls are 32 px; history management commands are 30 px. Sources has a separate back row, Artifacts places its list Back after Import, and model/history navigation relies on the global header. Native workspace headings vary between 16, 16.8 and 20/24 px. Code inspection found undefined `--ran-space-5` and `--ran-color-fill-secondary` usages, removing intended section spacing and hover feedback.

## Scope lock

- May change host `styles/assistant.css`, focused UI view composition (`sources-view.ts`, `artifacts-view.ts`, `settings-view.ts`, `panel.ts`, and their label adapter), a small local `view-header.ts` owner, existing/new host unit and browser tests, seven locale tables/types only if a new destination label is required, and design/acceptance docs.
- Keep the shared chat/runtime/provider/queue/storage/OAuth/native document contracts, vendor SDK, global ranui tokens, marketing pages and standalone page CTA scale unchanged. The site's documented 42 px page actions are a deliberate separate context.
- No new consent, implicit send/connect/download/save, destructive behavior or data format. Preserve pending-operation disablement, dirty-artifact confirmation, locale updates, navigation snapshots, IME, callback ownership and offline behavior.

## Design

Use existing tokens: `--ran-size-7` for 32 px desktop command frames; `--ran-text-label-3` for compact command labels, `--ran-text-label-2` for fields/list identity, `--ran-text-heading-3` for subpage titles, `--ran-text-label-1` for subsection titles. Use spaces 1/2/3/4/6 for 4/8/12/16/24 px relationships, `--ran-radius-sm` for controls, semantic background/foreground/border/link/danger tokens for every state. Do not invent missing ran tokens or replace them with arbitrary fallbacks. A host semantic control-size alias may combine these values; the existing 44 px coarse-pointer accessibility minimum is the only physical hit-target exception.

Ordinary commands are quiet ghost buttons with real enabled hover/pressed feedback. Explicit artifact save/accept commitments may use a compact primary treatment; destructive commitments remain clearly named and semantically styled. Disabled commands never look active or gain enabled hover. Resource and artifact rows keep readable identities and content-sized wrapping. Native and ranui fields share frame geometry; text editors/tables retain their content-driven sizes.

Use one flat header composition for Sources, Models, Conversations and Artifacts. Reuse the owner's actual heading and back button rather than moving navigation state into the helper. Back uses an ordinary accessible button with a small decorative chevron, consistent frame and destination-specific accessible label/tooltip. Each view continues to own its callback, localization and busy/dirty guards. Artifact list returns to conversation; editor/import returns to artifacts and preserves discard confirmation. Keep one back path per state and focus restoration after navigation; no footer Back mixed with save/import/export commands.

Keep titles, fields, help, lists and statuses on a shared 16 px inset. Use 8 px field/internal gaps, 16 px groups and 24 px sections. Remove invalid-token rules at their owner rather than layering a global override. Disclosures remain progressively revealed, scrollbars stay at the panel edge, and no new nested cards or ambient animation is added.

## Acceptance

Measure real browser geometry at 320/432 px panels and narrow windows, light/dark and coarse pointer. Check title/body leading edges, consistent control frames, real hover/pressed/disabled feedback, no overflow with long translations, and keyboard focus/return. Exercise Sources search/preview/attachment/connector failure, both model sources and advanced/offline visibility, history search/data confirmations, artifact list/import/editor/version/dirty Back/refine/export, result actions and queue. Controlled model/auth/voice responses remain explicitly distinct from real OAuth or model-quality validation.

The minimal alternative is CSS-only resizing; it cannot fix the inconsistent back path and busy/dirty ownership. A new UI framework or global component restyle is unnecessary. Adopt a small host-only header composition and existing view owners, then targeted regressions, full unit/coverage/lint/format/build and relevant browser suites, latest-head remote CI and final review.
42 changes: 35 additions & 7 deletions lib/agent-plugin/ui/artifacts-view.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createViewHeader, labelViewBack } from './view-header';
import { createArtifactPresentation, type ArtifactPresentationLabels } from './artifact-presentation';
import { validatePayload, payloadText } from '../artifacts/payload';
import {
Expand All @@ -14,6 +15,8 @@ export interface ArtifactsLabels extends ArtifactPresentationLabels {
title: string;
empty: string;
back: string;
backToConversation?: string;
backToList?: string;
titleField: string;
textField: string;
version: string;
Expand Down Expand Up @@ -48,9 +51,12 @@ export function createArtifactsView(options: {
}) {
const el = View('section').class('agent-artifacts').build();
const heading = View('h2').build();
heading.tabIndex = -1;
const header = createViewHeader(heading);
let backDestination: 'conversation' | 'list' = 'conversation';
const body = Div().build();
const status = View('p').class('agent-artifact-status').attr('role', 'status').build();
el.append(heading, body, status);
el.append(header, body, status);
let labels = options.labels;
let current: Artifact | undefined;
let activePresentation: ReturnType<typeof createArtifactPresentation> | undefined;
Expand All @@ -69,7 +75,10 @@ export function createArtifactsView(options: {
let cancelConfirmation: (() => void) | undefined;
const buttons = new Set<HTMLButtonElement>();
function button(key: keyof ArtifactsLabels, run: () => void): HTMLButtonElement {
const b = ButtonBuilder().attr('type', 'button').text(labels[key]).build();
const b = ButtonBuilder()
.attr('type', 'button')
.text(labels[key] ?? '')
.build();
b.dataset.action = key;
b.addEventListener('click', () => {
if (key !== 'discardChanges' && key !== 'cancel' && cancelConfirmation) {
Expand All @@ -82,6 +91,14 @@ export function createArtifactsView(options: {
buttons.add(b);
return b;
}
function setBack(back: HTMLButtonElement, destination: 'conversation' | 'list'): void {
backDestination = destination;
labelViewBack(
back,
(destination === 'conversation' ? labels.backToConversation : labels.backToList) ?? labels.back,
);
header.prepend(back);
}
function draftContent(text: string) {
const version = current?.versions.find(
(v) => String(v.version) === body.querySelector<HTMLSelectElement>('select')?.value,
Expand Down Expand Up @@ -109,7 +126,7 @@ export function createArtifactsView(options: {
invalid: 'importHint',
}[e.code] as keyof ArtifactsLabels)
: 'failed';
status.textContent = labels[key];
status.textContent = labels[key] ?? labels.failed;
if (e instanceof ArtifactError && (e.code === 'version-limit' || e.code === 'conflict')) {
status.append(
' ',
Expand Down Expand Up @@ -165,6 +182,7 @@ export function createArtifactsView(options: {
cancelConfirmation?.();
cancelConfirmation = undefined;
buttons.clear();
header.replaceChildren(heading);
body.replaceChildren();
status.textContent = '';
heading.textContent = labels.title;
Expand Down Expand Up @@ -201,6 +219,7 @@ export function createArtifactsView(options: {
void refresh();
}),
);
setBack(back, 'list');
const title = View('input')
.attr('type', 'text')
.attr('maxlength', '200')
Expand Down Expand Up @@ -317,7 +336,7 @@ export function createArtifactsView(options: {
.children(title, select, ...(source.hidden ? [text] : []), presentationHost, source, origin, actions)
.build();
if (presentation.kind !== 'text') form.insertBefore(actions, presentationHost);
body.append(back, form);
body.append(form);
const applyButton = actions.querySelector<HTMLButtonElement>('[data-action=apply]')!;
applyButton.hidden = !!a.versions.at(-1)!.payload && a.versions.at(-1)!.payload!.kind !== 'text';
captureDraft = () =>
Expand Down Expand Up @@ -456,12 +475,15 @@ export function createArtifactsView(options: {
status.textContent = labels.saved;
});
}),
);
setBack(
button('back', () =>
discard(() => {
dirty = false;
void refresh();
}),
),
'list',
);
data.addEventListener('input', () => {
dirty = true;
Expand All @@ -474,8 +496,8 @@ export function createArtifactsView(options: {
),
);

if (options.onClose) body.append(button('back', options.onClose));
if (el.isConnected) body.querySelector('button')?.focus();
if (options.onClose) setBack(button('back', options.onClose), 'conversation');
if (el.isConnected) heading.focus();
} catch (e) {
if (!disposed) error(e);
}
Expand Down Expand Up @@ -570,7 +592,12 @@ export function createArtifactsView(options: {
if (sourceSummary) sourceSummary.textContent = labels.textField + ' · JSON';
for (const b of buttons) {
const key = b.dataset.action as keyof ArtifactsLabels;
if (!b.dataset.artifact) b.textContent = labels[key];
if (key === 'back') {
labelViewBack(
b,
(backDestination === 'conversation' ? labels.backToConversation : labels.backToList) ?? labels.back,
);
} else if (!b.dataset.artifact) b.textContent = labels[key] ?? '';
}
for (const option of body.querySelectorAll('option')) option.textContent = `${labels.version} ${option.value}`;
const origin = body.querySelector('.agent-artifact-provenance');
Expand All @@ -594,6 +621,7 @@ export function createArtifactsView(options: {
for (const settle of pendingSaves) settle('cancelled');
++generation;
buttons.clear();
header.replaceChildren(heading);
el.remove();
},
get current() {
Expand Down
13 changes: 12 additions & 1 deletion lib/agent-plugin/ui/panel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createViewHeader, labelViewBack } from './view-header';
import { createTaskReceipt } from './task-receipt';
import { createResultActions } from './result-actions';
import { createWorkspaceNavigation } from './workspace-navigation';
Expand Down Expand Up @@ -3263,6 +3264,10 @@ export function createAgentPanel(
});
const { settingsHeading, modelActions, localCard, serviceCard, advanced, settingsHero, sourcePicker, offlineHint } =
settingsView;
const settingsBack = document.createElement('button');
settingsBack.type = 'button';
settingsBack.addEventListener('click', () => showView('chat'));
settingsHero.replaceChildren(createViewHeader(settingsHeading, settingsBack));
syncOfflineSettings = () => {
settingsView.setOnline(navigator.onLine !== false);
ggufUrl.hidden = navigator.onLine === false;
Expand Down Expand Up @@ -3338,7 +3343,11 @@ export function createAgentPanel(
},
onDelete: (id, trigger) => historyControls?.requestDelete(id, trigger),
});
historyView.append(conversationList.el, historyControls.el);
const historyBack = document.createElement('button');
historyBack.type = 'button';
historyBack.addEventListener('click', () => showView('chat'));
const historyHeading = conversationList.el.querySelector('h2')!;
historyView.append(createViewHeader(historyHeading, historyBack), conversationList.el, historyControls.el);
header.insertBefore(artifactsBtn, settingsBtn);
header.insertBefore(historyBtn, settingsBtn);
header.insertBefore(clearBtn, settingsBtn);
Expand Down Expand Up @@ -3396,6 +3405,8 @@ export function createAgentPanel(
});
const syncProductLabels = () => {
settingsView.syncLabels();
labelViewBack(settingsBack, t('agentBackToChat'));
labelViewBack(historyBack, t('agentBackToChat'));
sourceView.setLabels(sourcesLabels());
artifactsView.setLabels(artifactLabels());
artifactsView.setRefinementLabels(artifactWorkflowLabels());
Expand Down
6 changes: 3 additions & 3 deletions lib/agent-plugin/ui/sources-view.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createViewHeader, labelViewBack } from './view-header';
import { sourcePreview } from './source-preview';
import { sourceMatches } from '../connectors/reference-search';
import { renderSearchText } from './search-highlight';
Expand Down Expand Up @@ -223,8 +224,7 @@ export function createSourcesView(options: {
.children(driveHeading, driveHint, drive, driveSearch, driveList, driveDisconnect, driveRows, more)
.build();
el.append(
back,
heading,
createViewHeader(heading, back),
excerpt,
offlineHint,
status,
Expand Down Expand Up @@ -438,7 +438,7 @@ export function createSourcesView(options: {
offlineHint.textContent = labels.offline;
syncConnectivity();
excerpt.textContent = labels.excerpt;
back.textContent = labels.close;
labelViewBack(back, labels.close);
localHeading.textContent = labels.local;
attachFile.textContent = labels.attach;
syncConnectionActions();
Expand Down
16 changes: 16 additions & 0 deletions lib/agent-plugin/ui/view-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** Flat view chrome: callers retain navigation, labels and operation ownership. */
export function createViewHeader(heading: HTMLElement, back?: HTMLButtonElement): HTMLElement {
const header = document.createElement('div');
header.className = 'agent-view-header';
if (back) header.append(back);
header.append(heading);
return header;
}

export function labelViewBack(back: HTMLButtonElement, destination: string): void {
back.classList.add('agent-view-back');
back.setAttribute('aria-label', destination);
back.title = destination;
back.innerHTML =
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m14 6-6 6 6 6"/></svg>';
}
2 changes: 2 additions & 0 deletions lib/agent-plugin/ui/workspace-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export function artifactLabels(): ArtifactsLabels {
title: t('agentArtifactTitle'),
empty: t('agentArtifactEmpty'),
back: t('agentArtifactBack'),
backToConversation: t('agentBackToChat'),
backToList: t('agentArtifactBackToList'),
titleField: t('agentArtifactTitleField'),
textField: t('agentArtifactTextField'),
version: t('agentArtifactVersion'),
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/i18n/messages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ export const de: Partial<I18nMessages> = {
agentArtifactTitle: 'Artefakte',
agentArtifactEmpty: 'Nützliche Ergebnisse hier speichern, bearbeiten und lokal versionieren.',
agentArtifactBack: 'Zurück',
agentArtifactBackToList: 'Zurück zu den Ergebnissen',
agentArtifactTitleField: 'Titel',
agentArtifactTextField: 'Inhalt',
agentArtifactVersion: 'Version',
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/i18n/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ export const en: I18nMessages = {
agentArtifactTitle: 'Artifacts',
agentArtifactEmpty: 'Save useful results here to edit and keep versions on this device.',
agentArtifactBack: 'Back',
agentArtifactBackToList: 'Back to artifacts',
agentArtifactTitleField: 'Title',
agentArtifactTextField: 'Content',
agentArtifactVersion: 'Version',
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/i18n/messages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ export const es: Partial<I18nMessages> = {
agentArtifactTitle: 'Artefactos',
agentArtifactEmpty: 'Guarda resultados aquí para editarlos y conservar versiones en este dispositivo.',
agentArtifactBack: 'Volver',
agentArtifactBackToList: 'Volver a los resultados',
agentArtifactTitleField: 'Título',
agentArtifactTextField: 'Contenido',
agentArtifactVersion: 'Versión',
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/i18n/messages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ export const ja: Partial<I18nMessages> = {
agentArtifactTitle: '成果物',
agentArtifactEmpty: '結果を保存して、この端末で編集・バージョン管理できます。',
agentArtifactBack: '戻る',
agentArtifactBackToList: '成果物一覧に戻る',
agentArtifactTitleField: 'タイトル',
agentArtifactTextField: '内容',
agentArtifactVersion: 'バージョン',
Expand Down
Loading
Loading