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
6 changes: 3 additions & 3 deletions e2e/web-ui-sweep/profiles-scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ test('5. A blocked call: attribution chips, Allow in profile… and Why?', async
await expect(page.locator('[data-test^="activity-why-"]')).toBeFocused()
})

// 1280 is the first width that shows the Scope column; 768-1100 is where it used to clip Status and Duration.
// 1024 (lg) is the first width that shows the Scope column; the inline Why? needs 1280 (xl); 768-1100 is where it used to clip Status and Duration.
for (const width of [1440, 1280, 1100, 1024, 900, 768, 390]) {
test(`6. layout at ${width}px: no horizontal scroll, chips not clipped`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 })
Expand Down Expand Up @@ -301,8 +301,8 @@ for (const width of [1440, 1280, 1100, 1024, 900, 768, 390]) {
const target = await why.boundingBox()
expect(target!.width).toBeGreaterThanOrEqual(24)
expect(target!.height).toBeGreaterThanOrEqual(24)
if (width < 1280) {
// Below xl the Scope column folds away (it widened the table past its card); the drawer carries the chips.
if (width < 1024) {
// Below lg the Scope column folds away (it widened the table past its card); the drawer carries the chips.
await expect(page.locator('[data-test="activity-scope-col"]')).toBeHidden()
await expect(page.locator('[data-test="activity-drawer-attribution"]')).toBeVisible()
await expect(page.locator('[data-test="activity-drawer-attribution"] [data-test="attribution-client"]')).toBeVisible()
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/views/Activity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -585,9 +585,9 @@
Server {{ getSortIndicator('server_name') }}
</th>
<!-- Spec 108-j J10: who made the call and under which profile.
Folded away below md; the detail drawer carries the same
Folded away below lg (1024px); the detail drawer carries the same
chips in an "Attribution" section. -->
<th v-if="hasScopeColumn" class="hidden xl:table-cell" data-test="activity-scope-col">Scope</th>
<th v-if="hasScopeColumn" class="hidden lg:table-cell" data-test="activity-scope-col">Scope</th>
<th>Details</th>
<th v-if="hasSensitiveColumn" class="hidden lg:table-cell">Sensitive</th>
<!-- Intent carries the declared reason, not a 52px icon slot. -->
Expand Down Expand Up @@ -698,7 +698,7 @@
</router-link>
<span v-else class="text-base-content/40">-</span>
</td>
<td v-if="hasScopeColumn" class="hidden xl:table-cell max-w-[11rem]">
<td v-if="hasScopeColumn" class="hidden lg:table-cell max-w-[11rem]">
<AttributionChips :record="row.activity" :test-id="row.activity.id" stacked />
</td>
<td>
Expand Down Expand Up @@ -862,7 +862,18 @@
>
{{ statusPresentation(row.activity.status).label }}
</span>
<span v-else data-test="activity-status" class="sr-only">
<svg
v-else
data-test="activity-status-icon"
class="inline-block h-4 w-4 text-success"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<title>Success</title>
<path fill-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0L3.3 9.7a1 1 0 111.4-1.4l3.8 3.8 6.8-6.8a1 1 0 011.4 0z" clip-rule="evenodd" />
</svg>
<span v-if="!statusPresentation(row.activity.status).pill" data-test="activity-status" class="sr-only">
{{ statusPresentation(row.activity.status).label }}
</span>
<!-- Inline "Why?" only from xl: with the sidebar open the card is ~700px at 1024px and the
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/views/Clients.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
<div v-else-if="store.error" class="alert alert-error">{{ store.error }}</div>
<div v-else class="overflow-x-auto rounded-box border border-base-300 bg-base-100">
<table class="table"><thead><tr><th>Client</th><th>State</th><th>Last seen</th><th>Sessions</th><th>Config path</th><th>Profile</th></tr></thead>
<tbody><template v-for="client in store.clients" :key="client.id"><tr class="cursor-pointer hover" :class="focusedClient === client.id && 'bg-primary/10'" :data-test="focusedClient === client.id ? 'focused-client-row' : undefined" @click="toggle(client.id)"><td class="font-medium">{{ client.display_name }}</td><td><span class="badge badge-sm">{{ stateLabel(client.state) }}</span><button v-if="client.connection_unverified" type="button" class="btn btn-ghost btn-xs ml-2" data-test="check-client-connection" @click.stop="checkConnection(client.id)">Check connection</button></td><td>{{ relative(client.last_seen) }}</td><td>{{ client.active_sessions }}</td><td><code class="text-xs break-all">{{ client.display_path || '—' }}</code></td><td><ProfileChip :client="client" :auto-open="moveRequest === client.id" /></td></tr>
<tbody><template v-for="client in store.clients" :key="client.id"><tr class="cursor-pointer hover" :class="focusedClient === client.id && 'bg-primary/10'" :data-test="focusedClient === client.id ? 'focused-client-row' : undefined" @click="toggle(client.id)"><td class="font-medium"><button type="button" class="btn btn-ghost btn-xs btn-square mr-1 align-middle" :data-test="`client-expand-${client.id}`" :aria-expanded="expanded === client.id" :aria-controls="`client-details-${client.id}`" :aria-label="`Show details for ${client.display_name}`" @click.stop="toggle(client.id)"><svg class="h-4 w-4 transition-transform" :class="expanded === client.id && 'rotate-90'" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M7.2 4.2a1 1 0 011.4 0l5 5a1 1 0 010 1.4l-5 5a1 1 0 11-1.4-1.4L11.6 10 7.2 5.6a1 1 0 010-1.4z" clip-rule="evenodd" /></svg></button>{{ client.display_name }}</td><td><span class="badge badge-sm">{{ stateLabel(client.state) }}</span><button v-if="client.connection_unverified" type="button" class="btn btn-ghost btn-xs ml-2" data-test="check-client-connection" @click.stop="checkConnection(client.id)">Check connection</button></td><td>{{ relative(client.last_seen) }}</td><td>{{ client.active_sessions }}</td><td><code class="text-xs break-all">{{ client.display_path || '—' }}</code></td><td><ProfileChip :client="client" :auto-open="moveRequest === client.id" /></td></tr>
<tr v-if="rowErrors[client.id]" :data-test="`client-row-error-${client.id}`"><td colspan="6" class="bg-base-200/40"><GuardRefusal v-if="isGuardRefusal(rowErrors[client.id])" :refusal="rowErrors[client.id] as any" @navigate="bindings.clearRowError(client.id)" /><div v-else role="alert" class="alert alert-error text-sm">{{ describeError(rowErrors[client.id]) }}</div></td></tr>
<tr v-if="expanded === client.id"><td colspan="6" class="bg-base-200/40">
<tr v-if="expanded === client.id" :id="`client-details-${client.id}`"><td colspan="6" class="bg-base-200/40">
<p v-if="client.reload_hint" class="text-sm mb-2">{{ client.reload_hint }}</p>
<div v-if="clientScopeAvailable" class="flex flex-wrap gap-x-3 gap-y-1 mb-2 text-sm">
<router-link class="link" :data-test="`clients-row-link-activity-${client.id}`" :aria-label="`Activity for ${client.display_name}`" :to="scopeQuery.linkTo('activity', { view: 'calls', client: client.id, token: '' })">Activity</router-link>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
Browse Registry
Browse catalog
</router-link>
<router-link to="/security" class="btn btn-ghost btn-sm w-full gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/views/ServerDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@
>
Tools ({{ serverTools.length }})
</button>
<button :class="['tab tab-lg', activeTab === 'review' ? 'tab-active' : '']" @click="activeTab = 'review'">Review</button>
<button data-test="review-tab" :class="['tab tab-lg', activeTab === 'review' ? 'tab-active' : '']" @click="activeTab = 'review'">Review</button>
<button
:class="['tab tab-lg', activeTab === 'logs' ? 'tab-active' : '']"
@click="activeTab = 'logs'"
Expand Down Expand Up @@ -388,8 +388,8 @@
type="button"
data-test="server-tools-empty-security"
class="btn btn-sm btn-outline mt-4"
@click="openSecurityTab"
>View security findings</button>
@click="activeTab = 'review'"
>Open Review</button>
</div>

<div v-else class="space-y-4">
Expand Down Expand Up @@ -1752,9 +1752,9 @@ const toolsEmptyBody = computed(() => {
// Configuration is the right pointer because it renders `last_error` verbatim
// and unconditionally, which is exactly where the suppressed fault is legible.
if (server.value?.last_error) {
return "This server's tools are withheld while it is quarantined, and it last reported a connection error — so approving it may not be enough on its own. The error is shown above; review the findings on the Security tab as well."
return "This server's tools are withheld while it is quarantined, and it last reported a connection error — so approving it may not be enough on its own. The error is shown above; open the Review tab as well."
}
return "This server's tools are withheld while the server is quarantined. Review the findings on the Security tab, then approve the server to list them."
return "This server's tools are withheld while the server is quarantined. Open the Review tab to inspect the pending tools, then approve the server to list them."
})

// Tool quarantine (Spec 032)
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/Servers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@
class="btn btn-outline"
data-test="servers-empty-registry"
>
Browse Registry
Browse catalog
</router-link>
</div>
<p class="mt-4 text-sm">
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/views/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<!-- Search results (across all sections) -->
<div v-if="loaded && search.trim()" class="card bg-base-100 shadow-md" data-test="settings-search-results">
<div class="card-body">
<h2 class="card-title text-lg">🔍 Search results <span class="text-sm font-normal text-base-content/60">({{ filteredFields.length }})</span></h2>
<h2 class="card-title text-lg">Search results <span class="text-sm font-normal text-base-content/60">({{ filteredFields.length }})</span></h2>
<SettingsSection :key="`sec-${formEpoch}`" section-id="search" :fields="filteredFields" :working="state.working" :original="state.original" :locks="fieldLocks" @saved="onSectionSaved" />
</div>
</div>
Expand Down Expand Up @@ -60,7 +60,7 @@
<!-- Security & Access -->
<div v-show="activeTab === 'security'" class="card bg-base-100 shadow-md">
<div class="card-body">
<h2 class="card-title text-lg">🔒 Security &amp; Access</h2>
<h2 class="card-title text-lg">Security &amp; Access</h2>
<p class="text-sm text-base-content/60 mb-2">The settings that most affect how exposed and protected your instance is.</p>
<!-- connect-a-client helper -->
<div class="alert bg-base-200 border-base-300 mb-3 flex-col sm:flex-row items-start sm:items-center gap-2">
Expand Down Expand Up @@ -92,7 +92,7 @@
<!-- General -->
<div v-show="activeTab === 'general'" class="card bg-base-100 shadow-md">
<div class="card-body">
<h2 class="card-title text-lg">⚙️ General</h2>
<h2 class="card-title text-lg">General</h2>
<SettingsSection :key="`sec-${formEpoch}`" section-id="general" :fields="generalFields" :working="state.working" :original="state.original" :locks="fieldLocks" @saved="onSectionSaved" />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/settings/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ export const GENERAL_FIELDS: SettingField[] = [
// `teams` -> `server_edition` on load, and Settings.vue aliases it defensively
// so old configs hydrate the form while edits always save under `server_edition`.
export const SERVER_EDITION_TAB_LABEL = 'Server Edition'
export const SERVER_EDITION_SECTION_TITLE = '👥 Server Edition'
export const SERVER_EDITION_SECTION_TITLE = 'Server Edition'
//
// Spec 107 PR-B (T054): this row set is asserted EXACTLY by
// `tests/unit/settings-server-edition-wording.spec.ts`. The `Settings`
Expand Down
2 changes: 2 additions & 0 deletions frontend/tests/unit/activity-table-scanning.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ describe('Activity table — only failures are marked (F5)', () => {
expect(successCell.text()).toBe('Success')
// sr-only: present in the accessibility tree, absent from the scan.
expect(successCell.classes()).toContain('sr-only')
// I8a: a visible check icon with a Success title accompanies it.
expect(rows(wrapper)[0].find('[data-test="activity-status-icon"] title').text()).toBe('Success')

const errorCell = rows(wrapper)
.find(r => r.text().includes('Error'))!
Expand Down
21 changes: 21 additions & 0 deletions frontend/tests/unit/clients-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ describe('Clients page', () => {
expect(wrapper.find('a[href="/usage?client=cursor"]').exists()).toBe(true)
})

it('exposes a keyboard-operable expander button whose aria-expanded flips (I7)', async () => {
const router = makeRouter()
await router.push('/clients')
await router.isReady()
const wrapper = mount(Clients, {
global: { plugins: [router], stubs: { ClientConnectList: true, AgentTokens: true, ModeSwitcher: true } },
})
await flushPromises()
const btn = wrapper.find('[data-test="client-expand-cursor"]')
expect(btn.element.tagName).toBe('BUTTON')
expect(btn.attributes('aria-expanded')).toBe('false')
expect(btn.attributes('aria-label')).toContain('Show details for')
await btn.trigger('click')
await flushPromises()
expect(btn.attributes('aria-expanded')).toBe('true')
expect(api.getClient).toHaveBeenCalledTimes(1)
await btn.trigger('click')
await flushPromises()
expect(btn.attributes('aria-expanded')).toBe('false')
})

it('reads the tab from the URL and preserves unrelated query parameters when changing tabs', async () => {
const router = makeRouter()
await router.push('/clients?tab=endpoint&token=agent-1')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ const quarantined = (extra: ServerOverrides = {}): ServerOverrides => ({
...extra,
})

function empty_text(w: any): string {
return w.find('[data-test="server-tools-empty"]').text()
}

describe('ServerDetail — Tools tab empty state on a quarantined server (F08)', () => {
beforeEach(() => {
setActivePinia(createPinia())
Expand Down Expand Up @@ -131,15 +135,16 @@ describe('ServerDetail — Tools tab empty state on a quarantined server (F08)',
expect(text).not.toMatch(/\d/)
})

it('offers a way through to the Security tab where the findings live', async () => {
it('offers a way through to the Review tab', async () => {
const wrapper = await mountDetail(
quarantined({ quarantine: { pending_count: 2, changed_count: 0, blocked_count: 0 } })
)
const cta = wrapper.find('[data-test="server-tools-empty-security"]')
expect(cta.exists()).toBe(true)
expect(empty_text(wrapper)).not.toContain('Security tab')
await cta.trigger('click')
await flushPromises()
expect(wrapper.find('[data-test="security-tab"]').classes()).toContain('tab-active')
expect(wrapper.find('[data-test="review-tab"]').classes()).toContain('tab-active')
})

// Integrated-review finding. The withheld copy told the user to "approve the
Expand Down
Loading