From 3367ea039ad84dfeccf7df75a503a57672449e7e Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Sat, 19 Sep 2026 01:10:25 -0700 Subject: [PATCH 1/4] test(managed-skills): unsupported rows on every path, degraded on failed discovery #1017 review minors, fail-first on the real service: - the first snapshot after startup must already name the unsupported provider, not only after a save; - a target conflict on enable must keep that row beside the conflict; - an installed skill whose provider target discovery fails must read degraded with the resolution error, not unsupported with stale rows. Co-Authored-By: Claude Opus 5 (1M context) --- ...AgentCodeConventionsService.system.test.ts | 37 +++++++++++++++++++ ...tCodeInstalledSkillsService.system.test.ts | 25 ++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/main/agentCodeConventions/AgentCodeConventionsService.system.test.ts b/src/main/agentCodeConventions/AgentCodeConventionsService.system.test.ts index 62b4e25e5..6bb206383 100644 --- a/src/main/agentCodeConventions/AgentCodeConventionsService.system.test.ts +++ b/src/main/agentCodeConventions/AgentCodeConventionsService.system.test.ts @@ -455,6 +455,43 @@ describe('AgentCodeConventionsService', () => { ])) }) + // #1017 review: the informational `unsupported:*` rows were appended on + // two paths only. Every other path (the startup audit, a target conflict, + // a disable) replaced the list without them, so Settings stopped showing + // which provider cannot take the skill as soon as anything else happened. + it('shows the unsupported provider row from the first snapshot, before any save', async () => { + const root = await temporaryDirectory() + const currentTarget = target('agents-standard-personal-skills', join(root, '.agents', 'skills'), ['codex']) + const service = new AgentCodeConventionsService({ + stateFilePath: join(root, 'state', 'conventions.json'), + homeDirectory: root, + resolveTargets: async () => ({ targets: [currentTarget], unsupportedProviders: ['grok'] }), + }) + await service.initialize() + expect((await service.getSnapshot()).targets).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: 'unsupported:grok', state: 'unsupported' }), + ])) + }) + + it('keeps the unsupported provider row when enable stops on a target conflict', async () => { + const root = await temporaryDirectory() + const currentTarget = target('agents-standard-personal-skills', join(root, '.agents', 'skills'), ['codex']) + // A file the app does not own sits where the skill would go. + await writeFileWithParents(currentTarget.skillFile, '# Someone else\'s skill') + const service = new AgentCodeConventionsService({ + stateFilePath: join(root, 'state', 'conventions.json'), + homeDirectory: root, + resolveTargets: async () => ({ targets: [currentTarget], unsupportedProviders: ['grok'] }), + }) + await service.initialize() + const result = await service.save({ expectedRevision: 0, enabled: true, markdown: '# Rules' }) + expect(result).toMatchObject({ ok: false, code: 'target-conflict' }) + expect((await service.getSnapshot()).targets).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: 'unsupported:grok', state: 'unsupported' }), + expect.objectContaining({ id: 'agents-standard-personal-skills', state: 'conflict' }), + ])) + }) + it('still blocks enable when no registered provider supports personal skills', async () => { const root = await temporaryDirectory() const service = new AgentCodeConventionsService({ diff --git a/src/main/agentCodeConventions/AgentCodeInstalledSkillsService.system.test.ts b/src/main/agentCodeConventions/AgentCodeInstalledSkillsService.system.test.ts index aa1b007d5..a88d46dad 100644 --- a/src/main/agentCodeConventions/AgentCodeInstalledSkillsService.system.test.ts +++ b/src/main/agentCodeConventions/AgentCodeInstalledSkillsService.system.test.ts @@ -96,6 +96,8 @@ async function harness( now?: () => Date snapshotMaxBytes?: number unsupportedProviders?: ResolvedAgentCodeConventionsTargets['unsupportedProviders'] + /** Flip `.fail` to make provider target discovery throw from then on. */ + discovery?: { fail: boolean } } = {}, ) { const root = await temporaryDirectory() @@ -118,7 +120,10 @@ async function harness( installedSkillSnapshotRoot: join(root, 'state', 'managed-skill-snapshots'), installedSkillSnapshotMaxBytes: options.snapshotMaxBytes, homeDirectory: root, - resolveTargets: async () => resolved, + resolveTargets: async () => { + if (options.discovery?.fail) throw new Error('Could not read provider configuration') + return resolved + }, githubSkillSource, now: options.now ?? (() => new Date('2026-08-27T00:00:00.000Z')), operationId: (() => { let value = 0; return () => `installed-operation-${++value}` })(), @@ -172,6 +177,24 @@ describe('AgentCode installed skills service', () => { expect(snapshot.unsupportedProviders).toEqual(['grok']) }) + // #1017 review: when provider target discovery fails, the conventions + // and custom skills report 'degraded' with the error row, but an installed + // skill kept its stale rows, and with zero resolved targets its health + // read 'unsupported', which says "no provider can take this" when the + // truth is "we could not look". + it('reports an installed skill as degraded, with the error, when target discovery fails', async () => { + const discovery = { fail: false } + const { service, discoveries } = await harness({ discovery }) + const staged = stagedPackage({ commit: 'a'.repeat(40), files: [{ path: 'SKILL.md', content: '# Review code' }] }) + const found = await discoverOne(service, discoveries, staged) + await service.installGitHubSkills({ expectedRevision: 0, discoveryId: found.discoveryId, candidateIds: [staged.candidate.candidateId] }) + discovery.fail = true + await service.audit() + const skill = (await service.getInstalledSkillsSnapshot()).skills.find(item => item.name === 'review-code') + expect(skill?.health).toBe('degraded') + expect(skill?.targets).toEqual([expect.objectContaining({ id: 'provider-target-resolution', state: 'error' })]) + }) + it('installs a reviewed package and requires a second review before updating it', async () => { const { root, service, discoveries, skillDirectory } = await harness() const first = stagedPackage({ From bd5c29b5f349740664499d606f2a0bc255424da4 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Sat, 19 Sep 2026 01:10:25 -0700 Subject: [PATCH 2/4] fix(managed-skills): derive unsupported rows at snapshot time; failed discovery is degraded From the #1017 internal review (ledger T10): - The informational unsupported:* rows were appended by two of the roughly fifteen paths that set target statuses. The startup audit, a target conflict, a disable and the custom/installed reconciles dropped them. They are now added when a snapshot is built, from this.targets, by one builder that replaces three near-identical ones. Health reads the stored statuses, which never contain these rows. - resolveTargetsSafely invalidated conventions and custom skills on a discovery failure, but not installed skills. Those kept stale rows, and installedHealth then read unsupported, because it checked the zero-targets case before the error. Installed skills now get the error row, and installedHealth orders error first, as the other two do. Co-Authored-By: Claude Opus 5 (1M context) --- .../AgentCodeConventionsService.ts | 118 ++++++++---------- 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/src/main/agentCodeConventions/AgentCodeConventionsService.ts b/src/main/agentCodeConventions/AgentCodeConventionsService.ts index 8000bd82c..47362f292 100644 --- a/src/main/agentCodeConventions/AgentCodeConventionsService.ts +++ b/src/main/agentCodeConventions/AgentCodeConventionsService.ts @@ -1147,7 +1147,8 @@ export class AgentCodeManagedSkillsService { // targets would be a silent no-op write, so refuse it with the contract // the Settings UI already renders. if (request.enabled && this.targets.targets.length === 0) { - this.targetStatuses = this.unsupportedStatuses() + // The unsupported rows are added by snapshot(); see withUnsupportedRows. + this.targetStatuses = [] return { ok: false, code: 'unsupported', snapshot: this.snapshot() } } @@ -1197,10 +1198,6 @@ export class AgentCodeManagedSkillsService { } statuses.push(await this.publishTarget(item, rendered, desiredHash)) } - // Same informational rows the startup reconcile appends (#1014): the - // post-save snapshot must already show which providers cannot receive - // the skill, not only after a restart. - statuses.push(...this.unsupportedStatuses()) this.targetStatuses = statuses await this.persistBestEffort(statuses) return { ok: true, snapshot: this.snapshot() } @@ -1651,11 +1648,6 @@ export class AgentCodeManagedSkillsService { conflictFingerprint: this.installedOwnershipPolicy.retiredFingerprint(key, record), }) } - // Informational rows for providers without personal-skill support (#1014); - // installedHealth skips them when computing deployment health. - if (this.targets.unsupportedProviders.length > 0) { - statuses.push(...this.installedUnsupportedStatuses()) - } this.installedTargetStatuses.set(skill.id, statuses) } @@ -1711,7 +1703,7 @@ export class AgentCodeManagedSkillsService { ...skill, totalBytes: skill.files.reduce((total, file) => total + file.bytes, 0), health: this.installedHealth(skill, targets), - targets: [...targets], + targets: this.withUnsupportedRows(targets), } }), unsupportedProviders: [...this.targets.unsupportedProviders], @@ -1738,6 +1730,11 @@ export class AgentCodeManagedSkillsService { if (deployable.some(status => status.state === 'conflict' || status.state === 'retired')) { return 'conflict' } + // Error BEFORE the degenerate 'unsupported', as health() and customHealth() + // already order it (#1017 review): a failed target resolution also empties + // this.targets, and "no provider can take this" is the wrong thing to say + // when the truth is "we could not look". + if (deployable.some(status => status.state === 'error')) return 'degraded' if (this.targets.targets.length === 0) return 'unsupported' if (deployable.length === 0 || deployable.some(status => status.state !== 'installed')) { return 'degraded' @@ -1745,16 +1742,6 @@ export class AgentCodeManagedSkillsService { return 'active' } - private installedUnsupportedStatuses(): AgentCodeConventionsTargetStatus[] { - return this.targets.unsupportedProviders.map(provider => ({ - id: `unsupported:${provider}`, - providers: [provider], - displayPath: 'No personal skill directory', - state: 'unsupported', - message: `${provider} does not declare personal Agent Skills support.`, - })) - } - private installedStatus( target: AgentCodeConventionsTarget, state: AgentCodeConventionsTargetStatus['state'], @@ -1883,10 +1870,6 @@ export class AgentCodeManagedSkillsService { if (!next.pendingOperations[item.key]) statuses.push(this.customStatus(item.target, 'installed')) else statuses.push(await this.publishCustomTarget(updated, item, rendered, desiredHash)) } - // Same informational rows the startup reconcile appends (#1014): the - // post-enable snapshot must already show providers that cannot receive - // the skill, not only after a restart. - statuses.push(...this.customUnsupportedStatuses()) this.customTargetStatuses.set(skill.id, statuses) await this.persistBestEffort(statuses) return { ok: true, snapshot: this.customSnapshot() } @@ -2145,9 +2128,6 @@ export class AgentCodeManagedSkillsService { // made an otherwise complete enabled reconciliation look degraded. if (removed.state !== 'not-installed') statuses.push(removed) } - // WHY appended, not replacing (#1014): unsupported providers are informational - // per-target rows; real deployment rows must survive so health stays truthful. - statuses.push(...this.unsupportedStatuses()) this.targetStatuses = statuses await this.persistBestEffort(statuses) } @@ -2187,8 +2167,7 @@ export class AgentCodeManagedSkillsService { if (!normalized.ok) { this.customTargetStatuses.set( skill.id, - [...targets.targets.map(target => this.customStatus(target, 'error', normalized.message)), - ...this.customUnsupportedStatuses()], + targets.targets.map(target => this.customStatus(target, 'error', normalized.message)), ) return } @@ -2284,9 +2263,6 @@ export class AgentCodeManagedSkillsService { const removed = await this.removeCustomMaterialization(skill, key, record) if (removed.state !== 'not-installed') statuses.push(removed) } - // Informational rows for providers that cannot receive skills (#1014); - // deployment rows above remain the health input. - statuses.push(...this.customUnsupportedStatuses()) this.customTargetStatuses.set(skill.id, statuses) await this.persistBestEffort(statuses) } @@ -2962,18 +2938,25 @@ export class AgentCodeManagedSkillsService { state: 'error', message: safeErrorMessage(error), }] - // A previous successful audit may have left every custom target marked - // Installed. Once discovery itself fails, those paths are no longer a - // trustworthy statement about the current provider configuration; keep - // the desired definitions but invalidate deployment health together. + // A previous successful audit may have left every custom AND installed + // target marked Installed. Once discovery itself fails, those paths are + // no longer a trustworthy statement about the current provider + // configuration; keep the desired definitions but invalidate deployment + // health together. (Installed skills were missed here until the #1017 + // review: they kept their stale rows, and with zero resolved targets + // their health read 'unsupported'.) + const resolutionError: AgentCodeConventionsTargetStatus = { + id: 'provider-target-resolution', + providers: [], + displayPath: '', + state: 'error', + message: safeErrorMessage(error), + } for (const skill of Object.values(this.document.customSkills)) { - this.customTargetStatuses.set(skill.id, [{ - id: 'provider-target-resolution', - providers: [], - displayPath: '', - state: 'error', - message: safeErrorMessage(error), - }]) + this.customTargetStatuses.set(skill.id, [{ ...resolutionError }]) + } + for (const skill of Object.values(this.document.installedSkills)) { + this.installedTargetStatuses.set(skill.id, [{ ...resolutionError }]) } return false } @@ -3031,7 +3014,7 @@ export class AgentCodeManagedSkillsService { warnings: normalized.ok ? normalized.value.warnings : [], unsupportedProviders: this.targets.unsupportedProviders, recovery: this.recovery, - targets: [...this.targetStatuses].sort((left, right) => left.id.localeCompare(right.id)), + targets: this.withUnsupportedRows(this.targetStatuses).sort((left, right) => left.id.localeCompare(right.id)), } } @@ -3043,7 +3026,7 @@ export class AgentCodeManagedSkillsService { skills: Object.values(this.document.customSkills) .sort((left, right) => left.name.localeCompare(right.name)) .map(skill => { - const targets = [...(this.customTargetStatuses.get(skill.id) ?? [])] + const targets = this.withUnsupportedRows(this.customTargetStatuses.get(skill.id) ?? []) .sort((left, right) => left.id.localeCompare(right.id)) return { ...skill, @@ -3101,24 +3084,31 @@ export class AgentCodeManagedSkillsService { return 'disabled' } - private unsupportedStatuses(): AgentCodeConventionsTargetStatus[] { - return this.targets.unsupportedProviders.map(provider => ({ - id: `unsupported:${provider}`, - providers: [provider], - displayPath: '', - state: 'unsupported', - message: 'This provider does not declare personal Agent Skill support.', - })) - } - - private customUnsupportedStatuses(): AgentCodeConventionsTargetStatus[] { - return this.targets.unsupportedProviders.map(provider => ({ - id: `unsupported:${provider}`, - providers: [provider], - displayPath: '', - state: 'unsupported', - message: 'This provider does not declare personal Agent Skill support.', - })) + /** + * The informational rows for registered providers that declare no personal + * Agent Skills support (#1014), added to a surface's rows when a snapshot is + * built and never stored. + * + * WHY at snapshot time (#1017 review): the rows used to be appended by the + * code paths that set statuses, and only two of roughly fifteen did. The + * startup audit, a target conflict, a disable and every custom or installed + * reconcile replaced the list without them, so Settings stopped naming the + * unsupported provider as soon as anything else happened. The rows depend + * only on `this.targets`, so deriving them here makes every path agree. The + * three near-identical builders (conventions, custom, installed) are now + * one. Health never sees these rows: it reads the stored statuses. + */ + private withUnsupportedRows(statuses: readonly AgentCodeConventionsTargetStatus[]): AgentCodeConventionsTargetStatus[] { + return [ + ...statuses.filter(status => status.state !== 'unsupported'), + ...this.targets.unsupportedProviders.map(provider => ({ + id: `unsupported:${provider}`, + providers: [provider], + displayPath: 'No personal skill directory', + state: 'unsupported' as const, + message: `${provider} does not declare personal Agent Skills support.`, + })), + ] } private status( From b00330a7c0cb23ef3e3da8d88c2e34d2996c0b44 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Sat, 19 Sep 2026 01:10:25 -0700 Subject: [PATCH 3/4] chore: two leftover nits from the Grok review (ledger T6) Co-Authored-By: Claude Opus 5 (1M context) --- src/main/providerSwitch/transcriptEngine.ts | 2 +- src/providers/grok/runtime/grokSession.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/providerSwitch/transcriptEngine.ts b/src/main/providerSwitch/transcriptEngine.ts index 14593bd80..c42bf7cb4 100644 --- a/src/main/providerSwitch/transcriptEngine.ts +++ b/src/main/providerSwitch/transcriptEngine.ts @@ -2,7 +2,7 @@ // that projection model metadata must match capacity planning metadata. import { readFile } from 'fs/promises' import { homedir } from 'node:os' -import { join , dirname } from 'node:path' +import { dirname, join } from 'node:path' import { setTimeout as delay } from 'node:timers/promises' import { opencodeTranscriptFile } from 'opencode-terminal-headless' diff --git a/src/providers/grok/runtime/grokSession.ts b/src/providers/grok/runtime/grokSession.ts index 8937ec13d..ebcd5c25b 100644 --- a/src/providers/grok/runtime/grokSession.ts +++ b/src/providers/grok/runtime/grokSession.ts @@ -2,7 +2,7 @@ // // This class is a thin translator, the job OpencodeTerminalSession does for // OpenCode: it starts the app-owned helpers in the one order the recordings -// prove (leader leader → session over control → guard → prepared launch → +// prove (leader → session over control → guard → prepared launch → // terminal PTY), constructs GrokHeadless around them, and maps the package's // events onto the AgentSession contract SessionManager already speaks. // Everything about HOW native Grok is observed — acceptance, queue semantics, From c1dd488240a97ddfc250f4de6194a3e37413e8f3 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Sat, 19 Sep 2026 01:53:49 -0700 Subject: [PATCH 4/4] fix(managed-skills): a discovery failure never blocks deleting a disabled skill From the #1037 review (CHANGES REQUESTED): - MEDIUM: the new provider-target-resolution error row counted as a delete blocker. It has no fingerprint and so could never be approved, and deleting a disabled installed skill then dead-ended with "External changes must be reviewed" until discovery recovered. It is no longer a blocker. The stillOwnedKeys journal check still refuses to forget a skill whose files are on disk. Fail-first test. - LOW: the custom-skills list shows the provider on an unsupported row, where it used to show a generic path. - LOW: three comments still described the removed append mechanism. - NIT: an installed skill's error row shows its message. Co-Authored-By: Claude Opus 5 (1M context) --- .../AgentCodeConventionsService.ts | 29 ++++++++++++------- ...tCodeInstalledSkillsService.system.test.ts | 19 ++++++++++++ .../settings/ui/AgentCodeCustomSkillsRow.tsx | 4 ++- .../ui/AgentCodeInstalledSkillsRow.tsx | 2 +- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/main/agentCodeConventions/AgentCodeConventionsService.ts b/src/main/agentCodeConventions/AgentCodeConventionsService.ts index 47362f292..8c4099ae4 100644 --- a/src/main/agentCodeConventions/AgentCodeConventionsService.ts +++ b/src/main/agentCodeConventions/AgentCodeConventionsService.ts @@ -172,6 +172,10 @@ type StagedInstalledDiscovery = { expiresAtMs: number } +/** The status row a failed provider target discovery puts on every surface. + * It is a statement about discovery, not about any file on disk. */ +const TARGET_RESOLUTION_STATUS_ID = 'provider-target-resolution' + export class AgentCodeManagedSkillsService { private document = createEmptyAgentCodeConventionsDocument() private recovery: AgentCodeConventionsSnapshot['recovery'] @@ -686,9 +690,14 @@ export class AgentCodeManagedSkillsService { skill = this.document.installedSkills[request.skillId]! } const statuses = this.installedTargetStatuses.get(skill.id) ?? [] - const blockers = statuses.filter(status => status.state === 'conflict' + // The discovery-failure row is not a blocker (#1037 review): it names no + // file anyone could review, and it has no fingerprint, so it could never + // be approved. Deleting a disabled skill was then impossible until + // discovery recovered. What delete must never do, forget a skill whose + // files are still on disk, is the stillOwnedKeys journal check below. + const blockers = statuses.filter(status => status.id !== TARGET_RESOLUTION_STATUS_ID && (status.state === 'conflict' || status.state === 'retired' - || status.state === 'error') + || status.state === 'error')) const approvals = new Map((request.abandonTargets ?? []).map(value => [value.targetId, value])) const unresolved = blockers.filter(status => { const approval = approvals.get(status.id) @@ -1521,9 +1530,9 @@ export class AgentCodeManagedSkillsService { } private async reconcileInstalledSkillLocked(skill: AgentCodeInstalledSkillRecord): Promise { - // Unsupported providers no longer short-circuit (#1014); the informational - // rows are appended by applyInstalledOperationsLocked, the single funnel - // every enabled-skill status rebuild passes through. + // Unsupported providers no longer short-circuit (#1014). Their + // informational rows are added when a snapshot is built + // (withUnsupportedRows), not stored here. const targets = this.installedTargets(skill) if (skill.enabled) { try { @@ -2034,8 +2043,8 @@ export class AgentCodeManagedSkillsService { } private async reconcileEnabledLocked(): Promise { - // Unsupported providers no longer short-circuit (#1014): they contribute - // informational rows at the end instead of replacing deployment rows. + // Unsupported providers no longer short-circuit (#1014). Their + // informational rows are added at snapshot time (withUnsupportedRows). const normalized = normalizeAgentCodeConventionsMarkdown(this.document.markdown, { requireContent: true, }) @@ -2160,8 +2169,8 @@ export class AgentCodeManagedSkillsService { } private async reconcileCustomEnabledLocked(skill: AgentCodeCustomSkillRecord): Promise { - // Unsupported providers no longer short-circuit (#1014): they contribute - // informational rows at the exits instead of replacing deployment rows. + // Unsupported providers no longer short-circuit (#1014). Their + // informational rows are added at snapshot time (withUnsupportedRows). const targets = this.customTargets(skill) const normalized = normalizeAgentCodeCustomSkill(skill, { requireContent: true }) if (!normalized.ok) { @@ -2946,7 +2955,7 @@ export class AgentCodeManagedSkillsService { // review: they kept their stale rows, and with zero resolved targets // their health read 'unsupported'.) const resolutionError: AgentCodeConventionsTargetStatus = { - id: 'provider-target-resolution', + id: TARGET_RESOLUTION_STATUS_ID, providers: [], displayPath: '', state: 'error', diff --git a/src/main/agentCodeConventions/AgentCodeInstalledSkillsService.system.test.ts b/src/main/agentCodeConventions/AgentCodeInstalledSkillsService.system.test.ts index a88d46dad..136ef7904 100644 --- a/src/main/agentCodeConventions/AgentCodeInstalledSkillsService.system.test.ts +++ b/src/main/agentCodeConventions/AgentCodeInstalledSkillsService.system.test.ts @@ -195,6 +195,25 @@ describe('AgentCode installed skills service', () => { expect(skill?.targets).toEqual([expect.objectContaining({ id: 'provider-target-resolution', state: 'error' })]) }) + it('a disabled skill can still be deleted while target discovery is failing', async () => { + // #1037 review: the discovery-error row has no fingerprint, so as a + // delete blocker it could never be approved, and delete dead-ended with + // "External changes must be reviewed". + const discovery = { fail: false } + const { service, discoveries } = await harness({ discovery }) + const staged = stagedPackage({ commit: 'a'.repeat(40), files: [{ path: 'SKILL.md', content: '# Review code' }] }) + const found = await discoverOne(service, discoveries, staged) + const installed = await service.installGitHubSkills({ expectedRevision: 0, discoveryId: found.discoveryId, candidateIds: [staged.candidate.candidateId] }) + if (!installed.ok) throw new Error(JSON.stringify(installed)) + const skill = installed.snapshot.skills.find(item => item.name === 'review-code')! + const disabled = await service.setInstalledSkillEnabled({ expectedRevision: installed.snapshot.revision, skillId: skill.id, enabled: false }) + if (!disabled.ok) throw new Error(JSON.stringify(disabled)) + discovery.fail = true + await service.audit() + const deleted = await service.deleteInstalledSkill({ expectedRevision: disabled.snapshot.revision, skillId: skill.id }) + expect(deleted).toMatchObject({ ok: true }) + }) + it('installs a reviewed package and requires a second review before updating it', async () => { const { root, service, discoveries, skillDirectory } = await harness() const first = stagedPackage({ diff --git a/src/renderer/src/features/settings/ui/AgentCodeCustomSkillsRow.tsx b/src/renderer/src/features/settings/ui/AgentCodeCustomSkillsRow.tsx index eeefe59b0..b6ea7e06b 100644 --- a/src/renderer/src/features/settings/ui/AgentCodeCustomSkillsRow.tsx +++ b/src/renderer/src/features/settings/ui/AgentCodeCustomSkillsRow.tsx @@ -523,7 +523,9 @@ function TargetList({ skill, targets, onError }: {
{targets.map(target => (
- {target.displayPath || target.id} · {target.state} + {/* An unsupported row names the provider, which is the one thing it + is about; it has no path (#1037 review). */} + {target.state === 'unsupported' ? target.providers.join(' + ') : (target.displayPath || target.id)} · {target.state} {target.state === 'installed' || target.state === 'conflict' || target.state === 'retired' ? (