@cat-factory/app 0.242.2 → 0.244.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/board/RecurringPipelineModal.vue +12 -3
- package/app/components/panels/StepToolServers.logic.spec.ts +108 -1
- package/app/components/panels/StepToolServers.logic.ts +150 -2
- package/app/components/panels/StepToolServers.vue +89 -6
- package/app/components/settings/IssueTrackerPanel.vue +32 -205
- package/app/components/settings/TaskSourceCard.vue +143 -0
- package/app/stores/tasks.spec.ts +3 -0
- package/app/types/toolServers.ts +2 -0
- package/i18n/locales/de.json +19 -14
- package/i18n/locales/en.json +19 -14
- package/i18n/locales/es.json +19 -14
- package/i18n/locales/fr.json +19 -14
- package/i18n/locales/he.json +19 -14
- package/i18n/locales/it.json +19 -14
- package/i18n/locales/ja.json +19 -14
- package/i18n/locales/pl.json +19 -14
- package/i18n/locales/tr.json +19 -14
- package/i18n/locales/uk.json +19 -14
- package/package.json +2 -2
|
@@ -175,8 +175,11 @@ const intakeDispatch = computed<'queue' | 'per-ticket'>(() =>
|
|
|
175
175
|
isBugIntake.value ? 'queue' : 'per-ticket',
|
|
176
176
|
)
|
|
177
177
|
|
|
178
|
-
// Sources that can back intake right now
|
|
179
|
-
|
|
178
|
+
// Sources that can back intake right now: connected / App-installed AND enabled, AND able to run
|
|
179
|
+
// the predicate search intake fires. The last is not a refinement of the first two — a source
|
|
180
|
+
// without it saves a schedule that can never produce a ticket — so it is asked of the server
|
|
181
|
+
// (`supportsIntake`, derived from the registered provider) rather than inferred from the id here.
|
|
182
|
+
const intakeSources = computed(() => tasks.offeredSources.filter((s) => s.supportsIntake))
|
|
180
183
|
|
|
181
184
|
watch(open, (isOpen) => {
|
|
182
185
|
if (!isOpen) return
|
|
@@ -490,8 +493,14 @@ async function add() {
|
|
|
490
493
|
<p class="text-[11px] text-slate-500">
|
|
491
494
|
{{ t('board.recurring.intakeHint') }}
|
|
492
495
|
</p>
|
|
496
|
+
<!-- Two different remedies: connect something, versus connect something ELSE. A source
|
|
497
|
+
that is connected but cannot run a scheduled search is not an absent connection. -->
|
|
493
498
|
<p v-if="intakeSources.length === 0" class="text-[11px] text-amber-500">
|
|
494
|
-
{{
|
|
499
|
+
{{
|
|
500
|
+
tasks.anyOffered
|
|
501
|
+
? t('board.recurring.intakeNoIntakeSources')
|
|
502
|
+
: t('board.recurring.intakeNoSources')
|
|
503
|
+
}}
|
|
495
504
|
</p>
|
|
496
505
|
<div v-else class="flex flex-wrap gap-1">
|
|
497
506
|
<UButton
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
+
KNOWN_OBSERVED_STATUSES,
|
|
3
4
|
KNOWN_REASONS,
|
|
5
|
+
OBSERVED_STATUS_KEY,
|
|
4
6
|
REASON_KEY,
|
|
5
7
|
REMEDY_KEY,
|
|
8
|
+
observationFor,
|
|
9
|
+
observationIsFault,
|
|
10
|
+
observationText,
|
|
6
11
|
reasonText,
|
|
7
12
|
remedyText,
|
|
13
|
+
unattributedObservations,
|
|
8
14
|
} from './StepToolServers.logic'
|
|
9
|
-
import type { ToolServerUnavailableReason } from '~/types/toolServers'
|
|
15
|
+
import type { ObservedToolServer, ToolServerUnavailableReason } from '~/types/toolServers'
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
18
|
* A dropped tool server is the whole point of this surface: until it existed, a run that quietly
|
|
@@ -98,3 +104,104 @@ function render(reason: string): { key: string; params?: Record<string, unknown>
|
|
|
98
104
|
})
|
|
99
105
|
return seen
|
|
100
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The OBSERVED half: what the agent's CLI said about the servers the platform wired. Its whole
|
|
110
|
+
* value is a set of distinctions, and each one collapses into "looks fine" if it is lost — so
|
|
111
|
+
* these pin the distinctions rather than the copy.
|
|
112
|
+
*/
|
|
113
|
+
describe('tool-server startup observations', () => {
|
|
114
|
+
it('gives every observed status but `ready` its own copy', () => {
|
|
115
|
+
// `ready` is deliberately absent: a started server's line depends on its tool count, which is
|
|
116
|
+
// three sentences rather than one key. Everything else is exhaustive against the schema, so a
|
|
117
|
+
// member added on the backend fails here instead of rendering blank.
|
|
118
|
+
expect([...Object.keys(OBSERVED_STATUS_KEY), 'ready'].sort()).toEqual(
|
|
119
|
+
[...KNOWN_OBSERVED_STATUSES].sort(),
|
|
120
|
+
)
|
|
121
|
+
const keys = Object.values(OBSERVED_STATUS_KEY)
|
|
122
|
+
expect(new Set(keys).size).toBe(keys.length)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('says NOTHING when no observation was made', () => {
|
|
126
|
+
// The distinction the whole field rests on. A codex run, an image one version behind and an
|
|
127
|
+
// unmapped runner pool all report nothing, and any placeholder here would read as a verdict
|
|
128
|
+
// about a server that was very likely fine.
|
|
129
|
+
expect(observationFor(undefined, 'slack')).toBeNull()
|
|
130
|
+
expect(observationText(null, (key) => key)).toBeNull()
|
|
131
|
+
expect(observationIsFault(null)).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('distinguishes “nobody looked” from “the CLI never loaded it”', () => {
|
|
135
|
+
// Both render as an absent server on the chip if they are conflated, and only the second is
|
|
136
|
+
// evidence of anything. The first must never raise a fault.
|
|
137
|
+
const notLoaded = observationFor([{ id: 'jira', status: 'ready' }], 'slack')
|
|
138
|
+
expect(notLoaded).toEqual({ kind: 'not_loaded' })
|
|
139
|
+
expect(observationText(notLoaded, (key) => key)).toBe(
|
|
140
|
+
'panels.stepDetail.toolServers.observed.notLoaded',
|
|
141
|
+
)
|
|
142
|
+
expect(observationIsFault(notLoaded)).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('separates “started with no tools” from “started, tools uncounted”', () => {
|
|
146
|
+
// A server that connected and exposes nothing reaches the agent exactly like one that was
|
|
147
|
+
// never wired, and every other signal about it says healthy — so it gets its own sentence
|
|
148
|
+
// rather than a "0 tools" that reads as a rendering artefact.
|
|
149
|
+
const none = observationFor([{ id: 'slack', status: 'ready', toolCount: 0 }], 'slack')
|
|
150
|
+
const uncounted = observationFor([{ id: 'slack', status: 'ready' }], 'slack')
|
|
151
|
+
const some = observationFor([{ id: 'slack', status: 'ready', toolCount: 3 }], 'slack')
|
|
152
|
+
expect(observationText(none, (key) => key)).toBe(
|
|
153
|
+
'panels.stepDetail.toolServers.observed.readyNoTools',
|
|
154
|
+
)
|
|
155
|
+
expect(observationText(uncounted, (key) => key)).toBe(
|
|
156
|
+
'panels.stepDetail.toolServers.observed.ready',
|
|
157
|
+
)
|
|
158
|
+
expect(observationText(some, (key) => key)).toBe(
|
|
159
|
+
'panels.stepDetail.toolServers.observed.readyTools',
|
|
160
|
+
)
|
|
161
|
+
// None of the three is a fault: the count is a diagnosis for a person to read, not a verdict
|
|
162
|
+
// the surface should paint red.
|
|
163
|
+
for (const observation of [none, uncounted, some]) {
|
|
164
|
+
expect(observationIsFault(observation)).toBe(false)
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('flags the two states the platform promised a tool it did not get', () => {
|
|
169
|
+
for (const status of ['failed', 'needs_auth'] as const) {
|
|
170
|
+
expect(observationIsFault(observationFor([{ id: 'slack', status }], 'slack'))).toBe(true)
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('never flags a state the report did not resolve', () => {
|
|
175
|
+
// `unknown` is a fact about THIS build (a word it could not map) or about the moment the
|
|
176
|
+
// report was taken (a server still handshaking), never about the server itself. Painting it
|
|
177
|
+
// as a fault would send an operator to debug a working integration every time a CLI adds a
|
|
178
|
+
// status word or starts a server a moment slower than the session announcement.
|
|
179
|
+
// Cast, because the point is a value the TYPE excludes and the DATA carries: the vocabulary
|
|
180
|
+
// is persisted on a run, so a status recorded by a newer harness (or retired since) reads back
|
|
181
|
+
// here as a member this build has no case for.
|
|
182
|
+
const persisted = observationFor(
|
|
183
|
+
[{ id: 'slack', status: 'reticulating' } as unknown as ObservedToolServer],
|
|
184
|
+
'slack',
|
|
185
|
+
)
|
|
186
|
+
expect(persisted).toEqual({ kind: 'loaded', status: 'unknown' })
|
|
187
|
+
expect(observationIsFault(persisted)).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('states a report naming servers the dispatch did not wire', () => {
|
|
191
|
+
// Empty on every ordinary run (`--strict-mcp-config`). The only way to reach it is a producer
|
|
192
|
+
// describing some OTHER job, and silently filtering those rows would present that report as
|
|
193
|
+
// this run's clean bill of health.
|
|
194
|
+
const record = {
|
|
195
|
+
agentKind: 'coder',
|
|
196
|
+
wired: [{ id: 'slack', label: 'Slack', transport: 'http' as const }],
|
|
197
|
+
unavailable: [],
|
|
198
|
+
observed: [
|
|
199
|
+
{ id: 'slack', status: 'ready' as const },
|
|
200
|
+
{ id: 'stranger', status: 'ready' as const },
|
|
201
|
+
],
|
|
202
|
+
}
|
|
203
|
+
expect(unattributedObservations(record).map((s) => s.id)).toEqual(['stranger'])
|
|
204
|
+
// …and nothing to state when there was no report at all.
|
|
205
|
+
expect(unattributedObservations({ ...record, observed: undefined })).toEqual([])
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
toolServerObservedStatusSchema,
|
|
3
|
+
toolServerUnavailableReasonSchema,
|
|
4
|
+
} from '@cat-factory/contracts'
|
|
5
|
+
import type {
|
|
6
|
+
ObservedToolServer,
|
|
7
|
+
StepToolServers,
|
|
8
|
+
ToolServerObservedStatus,
|
|
9
|
+
ToolServerUnavailableReason,
|
|
10
|
+
} from '~/types/toolServers'
|
|
3
11
|
|
|
4
12
|
// The step surface's tool-server (MCP) reason mapping. Kept out of the SFC on the same seam as
|
|
5
13
|
// `StepTestReport.logic.ts`, so the vocabulary rule can be asserted without mounting a component.
|
|
@@ -102,3 +110,143 @@ export function remedyText(
|
|
|
102
110
|
): string | null {
|
|
103
111
|
return isKnownReason(reason) ? t(REMEDY_KEY[reason]) : null
|
|
104
112
|
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// The OBSERVED half: what the agent's CLI reported about the servers it loaded.
|
|
116
|
+
//
|
|
117
|
+
// The mapping above answers "why did the platform withhold this tool". This one answers the
|
|
118
|
+
// question that mapping structurally cannot: a server that passed every check, was promised to the
|
|
119
|
+
// agent in its prompt, and then did not come up. The two are rendered together on one chip because
|
|
120
|
+
// they are two halves of one answer about one server — but they are never merged into one status,
|
|
121
|
+
// because a withheld server and a failed one need different people to fix different things.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* What this build knows about one WIRED server after joining the CLI's report onto it.
|
|
126
|
+
*
|
|
127
|
+
* `null` and `not_loaded` are the pair this whole type exists to keep apart, and conflating them is
|
|
128
|
+
* the mistake that would make the surface lie. `null` means NO OBSERVATION WAS MADE — the run's
|
|
129
|
+
* harness publishes no report (codex's CLI does not, nor does any image older than the one that
|
|
130
|
+
* introduced this, nor an unmapped runner pool) — so nothing at all is known about whether the
|
|
131
|
+
* server started. `not_loaded` means an observation WAS made and this server was not in it, which
|
|
132
|
+
* is positive evidence that the CLI never loaded it. Rendering the first as the second would
|
|
133
|
+
* accuse every wired server on every deployment one image behind.
|
|
134
|
+
*/
|
|
135
|
+
export type ToolServerObservation =
|
|
136
|
+
| null
|
|
137
|
+
| { kind: 'loaded'; status: ToolServerObservedStatus; toolCount?: number }
|
|
138
|
+
| { kind: 'not_loaded' }
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Join the CLI's report onto one wired server id.
|
|
142
|
+
*
|
|
143
|
+
* Takes the whole `observed` field rather than a prepared map so the absent-vs-empty decision is
|
|
144
|
+
* made HERE, in the one place that owns the distinction, instead of by each caller building the
|
|
145
|
+
* map. An absent field yields `null` for every server; a present one yields either the CLI's line
|
|
146
|
+
* or `not_loaded`.
|
|
147
|
+
*/
|
|
148
|
+
export function observationFor(
|
|
149
|
+
observed: readonly ObservedToolServer[] | undefined,
|
|
150
|
+
id: string,
|
|
151
|
+
): ToolServerObservation {
|
|
152
|
+
if (!observed) return null
|
|
153
|
+
const found = observed.find((server) => server.id === id)
|
|
154
|
+
if (!found) return { kind: 'not_loaded' }
|
|
155
|
+
return {
|
|
156
|
+
kind: 'loaded',
|
|
157
|
+
status: isKnownObservedStatus(found.status) ? found.status : 'unknown',
|
|
158
|
+
// Carried only when the CLI counted, and `0` is carried: a server that connected and exposed
|
|
159
|
+
// nothing reaches the agent exactly like one that was never wired, so it is the most
|
|
160
|
+
// diagnostic count on the field and the one a truthiness guard would erase.
|
|
161
|
+
...(typeof found.toolCount === 'number' ? { toolCount: found.toolCount } : {}),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The servers the CLI named that this step's record does not list as wired.
|
|
167
|
+
*
|
|
168
|
+
* Empty by construction on every ordinary run: the harness starts the CLI under
|
|
169
|
+
* `--strict-mcp-config`, so the only servers it can load are the ones this dispatch wrote for it.
|
|
170
|
+
* Surfaced anyway rather than filtered out, because the one way to reach it is a producer whose
|
|
171
|
+
* report does not describe this job — a runner-pool manifest pointing `toolServersPath` at the
|
|
172
|
+
* wrong field, say — and a surface that silently dropped those rows would present a report about
|
|
173
|
+
* some other job as a clean bill of health for this one.
|
|
174
|
+
*/
|
|
175
|
+
export function unattributedObservations(record: StepToolServers): ObservedToolServer[] {
|
|
176
|
+
if (!record.observed) return []
|
|
177
|
+
const wired = new Set(record.wired.map((server) => server.id))
|
|
178
|
+
return record.observed.filter((server) => !wired.has(server.id))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The i18n key per observed status, for a server the CLI DID name. An exhaustive `Record` for the
|
|
183
|
+
* same reason {@link REASON_KEY} is one: a member added to the wire vocabulary must fail to
|
|
184
|
+
* compile here rather than render as a blank line.
|
|
185
|
+
*
|
|
186
|
+
* `ready` is deliberately absent — a started server's line depends on its tool COUNT, which is
|
|
187
|
+
* three separate sentences (counted some, counted none, did not count), so it is resolved by
|
|
188
|
+
* {@link observationText} rather than by a single key.
|
|
189
|
+
*/
|
|
190
|
+
export const OBSERVED_STATUS_KEY: Record<Exclude<ToolServerObservedStatus, 'ready'>, string> = {
|
|
191
|
+
failed: 'panels.stepDetail.toolServers.observed.failed',
|
|
192
|
+
needs_auth: 'panels.stepDetail.toolServers.observed.needsAuth',
|
|
193
|
+
unknown: 'panels.stepDetail.toolServers.observed.unknown',
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The observed-status vocabulary as the SCHEMA states it: what a parity assertion grades against. */
|
|
197
|
+
export const KNOWN_OBSERVED_STATUSES = toolServerObservedStatusSchema.options
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether a persisted status is a member THIS build knows, derived from the picklist's own options.
|
|
201
|
+
* A predicate rather than a truthiness check on the lookup, for the reason {@link isKnownReason}
|
|
202
|
+
* gives: an `Object.prototype` member name reads back as a truthy non-key.
|
|
203
|
+
*/
|
|
204
|
+
export function isKnownObservedStatus(status: string): status is ToolServerObservedStatus {
|
|
205
|
+
return (KNOWN_OBSERVED_STATUSES as readonly string[]).includes(status)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Render one observation, or `null` when there is nothing to say.
|
|
210
|
+
*
|
|
211
|
+
* `null` is returned for `null` — no observation was made — and it is the whole reason this
|
|
212
|
+
* function exists rather than a template expression: the surface must be BYTE-FOR-BYTE what it was
|
|
213
|
+
* before this field existed on every run that carries no report, which is every codex run, every
|
|
214
|
+
* run on an image one version behind, and every run on an unmapped runner pool. Silence is the
|
|
215
|
+
* only honest rendering of "nobody looked".
|
|
216
|
+
*/
|
|
217
|
+
export function observationText(
|
|
218
|
+
observation: ToolServerObservation,
|
|
219
|
+
t: (key: string, params?: Record<string, unknown>) => string,
|
|
220
|
+
): string | null {
|
|
221
|
+
if (observation === null) return null
|
|
222
|
+
if (observation.kind === 'not_loaded') {
|
|
223
|
+
return t('panels.stepDetail.toolServers.observed.notLoaded')
|
|
224
|
+
}
|
|
225
|
+
if (observation.status !== 'ready') return t(OBSERVED_STATUS_KEY[observation.status])
|
|
226
|
+
if (observation.toolCount === undefined) {
|
|
227
|
+
return t('panels.stepDetail.toolServers.observed.ready')
|
|
228
|
+
}
|
|
229
|
+
// Zero gets its own sentence rather than "0 tools": a server that started and offers nothing is
|
|
230
|
+
// a distinct fault (a narrowed `allowedTools` matching nothing, a vendor that authenticated and
|
|
231
|
+
// served an empty catalog), and it is the one an operator would otherwise never suspect,
|
|
232
|
+
// because every other signal about it says healthy.
|
|
233
|
+
return observation.toolCount === 0
|
|
234
|
+
? t('panels.stepDetail.toolServers.observed.readyNoTools')
|
|
235
|
+
: t('panels.stepDetail.toolServers.observed.readyTools', { count: observation.toolCount })
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Whether an observation should DRAW ATTENTION on the chip: the server was promised to the agent
|
|
240
|
+
* and the CLI could not deliver it.
|
|
241
|
+
*
|
|
242
|
+
* `unknown` is deliberately NOT alarming. It covers a word this build could not map (a fact about
|
|
243
|
+
* this build rather than about the server) and a server the CLI reported as still handshaking when
|
|
244
|
+
* it announced the session (a fact about the moment the report was taken). Neither says anything
|
|
245
|
+
* happened to the server, and painting either as a fault would send an operator to debug a working
|
|
246
|
+
* integration every time a CLI adds a status or starts a server a moment slower.
|
|
247
|
+
*/
|
|
248
|
+
export function observationIsFault(observation: ToolServerObservation): boolean {
|
|
249
|
+
if (observation === null) return false
|
|
250
|
+
if (observation.kind === 'not_loaded') return true
|
|
251
|
+
return observation.status === 'failed' || observation.status === 'needs_auth'
|
|
252
|
+
}
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
// unavailable server is a chip of its own with its own translated reason, never a shorter list of
|
|
9
9
|
// the wired ones: "absent" and "zero" must not render the same.
|
|
10
10
|
//
|
|
11
|
+
// The wired half carries a SECOND answer when the run's harness reported one: what the agent's
|
|
12
|
+
// CLI said about each server when it started up. The two are deliberately not merged into one
|
|
13
|
+
// status — the platform withholding a tool and the CLI failing to start one are different faults
|
|
14
|
+
// for different people — and the absence of a CLI report renders as nothing at all, because
|
|
15
|
+
// "nobody looked" and "the CLI loaded nothing" are opposite facts and only silence states the
|
|
16
|
+
// first one honestly (a codex run, an older image and an unmapped runner pool all reach it).
|
|
17
|
+
//
|
|
11
18
|
// SELF-HIDING when the record holds nothing, like the sibling panels around it. The record is
|
|
12
19
|
// written on EVERY container dispatch, so both lists empty is the state of every step on every
|
|
13
20
|
// deployment that registers no tool servers at all — the overwhelming default. That state is a
|
|
@@ -16,7 +23,14 @@
|
|
|
16
23
|
// section on every step of every run to say nothing happened. The distinction absent-vs-empty is
|
|
17
24
|
// still carried on the wire and answered by the debug API, which is where a reader asking it looks.
|
|
18
25
|
import type { StepToolServers } from '~/types/toolServers'
|
|
19
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
observationFor,
|
|
28
|
+
observationIsFault,
|
|
29
|
+
observationText,
|
|
30
|
+
reasonText,
|
|
31
|
+
remedyText,
|
|
32
|
+
unattributedObservations,
|
|
33
|
+
} from '~/components/panels/StepToolServers.logic'
|
|
20
34
|
import { agentKindMeta } from '~/utils/catalog'
|
|
21
35
|
|
|
22
36
|
const props = defineProps<{
|
|
@@ -33,8 +47,22 @@ const props = defineProps<{
|
|
|
33
47
|
|
|
34
48
|
const { t } = useI18n()
|
|
35
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Servers the CLI named that this dispatch did not wire. Empty on every ordinary run (the CLI runs
|
|
52
|
+
* under `--strict-mcp-config`), and surfaced rather than filtered because the only way to reach it
|
|
53
|
+
* is a report that describes some other job — which must not be presented as this job's clean bill
|
|
54
|
+
* of health.
|
|
55
|
+
*/
|
|
56
|
+
const unattributed = computed(() => unattributedObservations(props.toolServers))
|
|
57
|
+
|
|
36
58
|
const hasAny = computed(
|
|
37
|
-
() =>
|
|
59
|
+
() =>
|
|
60
|
+
props.toolServers.wired.length > 0 ||
|
|
61
|
+
props.toolServers.unavailable.length > 0 ||
|
|
62
|
+
// A report about servers this dispatch did not wire is the one thing worth showing on an
|
|
63
|
+
// otherwise empty record: both lists empty says the dispatched kind declared none, and a CLI
|
|
64
|
+
// naming servers anyway contradicts exactly that.
|
|
65
|
+
unattributed.value.length > 0,
|
|
38
66
|
)
|
|
39
67
|
|
|
40
68
|
/** Set only when a helper re-dispatch owns this record, so the ordinary case renders no extra line. */
|
|
@@ -44,6 +72,22 @@ const dispatchedAs = computed(() =>
|
|
|
44
72
|
: null,
|
|
45
73
|
)
|
|
46
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Each wired server with the CLI's own verdict joined onto it, when a verdict was reported. Bound
|
|
77
|
+
* here for the same reason `drops` is: the join and the absent-vs-not-loaded decision are pure and
|
|
78
|
+
* assertable without mounting, and resolving them once per record beats once per re-render.
|
|
79
|
+
*/
|
|
80
|
+
const wired = computed(() =>
|
|
81
|
+
props.toolServers.wired.map((server) => {
|
|
82
|
+
const observation = observationFor(props.toolServers.observed, server.id)
|
|
83
|
+
return {
|
|
84
|
+
...server,
|
|
85
|
+
observedText: observationText(observation, (key, params) => t(key, params ?? {})),
|
|
86
|
+
isFault: observationIsFault(observation),
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
|
|
47
91
|
/**
|
|
48
92
|
* Each dropped server with both halves of its answer resolved: WHY it was dropped, and what to
|
|
49
93
|
* change so the next run gets it. Bound here rather than called from the template so the pure
|
|
@@ -80,20 +124,59 @@ const drops = computed(() =>
|
|
|
80
124
|
{{ t('panels.stepDetail.toolServers.dispatchedAs', { agent: dispatchedAs }) }}
|
|
81
125
|
</p>
|
|
82
126
|
|
|
83
|
-
<ul v-if="
|
|
127
|
+
<ul v-if="wired.length" class="flex flex-wrap gap-1.5">
|
|
84
128
|
<li
|
|
85
|
-
v-for="server in
|
|
129
|
+
v-for="server in wired"
|
|
86
130
|
:key="server.id"
|
|
87
131
|
data-testid="step-tool-server-wired"
|
|
88
|
-
|
|
132
|
+
:data-observed-fault="server.isFault ? 'true' : undefined"
|
|
133
|
+
class="inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[12px]"
|
|
134
|
+
:class="
|
|
135
|
+
server.isFault
|
|
136
|
+
? 'border-amber-500/30 bg-amber-500/10 text-amber-200'
|
|
137
|
+
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200'
|
|
138
|
+
"
|
|
89
139
|
:title="
|
|
90
140
|
server.tools?.length
|
|
91
141
|
? t('panels.stepDetail.toolServers.narrowed', { tools: server.tools.join(', ') })
|
|
92
142
|
: t('panels.stepDetail.toolServers.allTools')
|
|
93
143
|
"
|
|
94
144
|
>
|
|
95
|
-
<UIcon
|
|
145
|
+
<UIcon
|
|
146
|
+
:name="server.isFault ? 'i-lucide-triangle-alert' : 'i-lucide-check'"
|
|
147
|
+
class="h-3.5 w-3.5"
|
|
148
|
+
/>
|
|
96
149
|
<span>{{ server.label || server.id }}</span>
|
|
150
|
+
<!--
|
|
151
|
+
The CLI's own verdict, when the run's harness reported one. Absent renders NOTHING: a
|
|
152
|
+
harness that publishes no report has said nothing about this server, and a placeholder
|
|
153
|
+
would read as a verdict.
|
|
154
|
+
-->
|
|
155
|
+
<span
|
|
156
|
+
v-if="server.observedText"
|
|
157
|
+
data-testid="step-tool-server-observed"
|
|
158
|
+
class="opacity-80"
|
|
159
|
+
>{{ server.observedText }}</span
|
|
160
|
+
>
|
|
161
|
+
</li>
|
|
162
|
+
</ul>
|
|
163
|
+
|
|
164
|
+
<!--
|
|
165
|
+
A report naming servers this dispatch did not wire can only come from a producer describing
|
|
166
|
+
some other job (a runner-pool manifest mapped at the wrong field). Stated rather than
|
|
167
|
+
filtered, so the rest of the report is not read as authoritative about this run.
|
|
168
|
+
-->
|
|
169
|
+
<ul v-if="unattributed.length" class="mt-2 space-y-1.5">
|
|
170
|
+
<li
|
|
171
|
+
v-for="server in unattributed"
|
|
172
|
+
:key="server.id"
|
|
173
|
+
data-testid="step-tool-server-unattributed"
|
|
174
|
+
class="flex items-start gap-1.5 text-[12px] text-slate-400"
|
|
175
|
+
>
|
|
176
|
+
<UIcon name="i-lucide-circle-help" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
177
|
+
<span>{{
|
|
178
|
+
t('panels.stepDetail.toolServers.observed.unattributed', { id: server.id })
|
|
179
|
+
}}</span>
|
|
97
180
|
</li>
|
|
98
181
|
</ul>
|
|
99
182
|
|