fix(hud): lead with the state tag, keep feedState in get_hud_caption

Review fixes for the HUD summary language change:

- The translated feed-state tag now leads the one-line summary and the
  fallback line, so the readout's ellipsis cannot hide it.
- get_hud_caption checks the reply's feedState, returns it and appends the
  state tag; a reply naming another state keeps only the place.
- The model gets the layer names the user sees (localizeLayerName).
- Japanese and Chinese summaries are capped at 20 characters; pt, zh-CN and
  zh-Hant get explicit language names.
- A partial view is narrated as incomplete coverage, not as not live.
- provenanceTag and hudSummaryMatchesProvenance move to layerSnapshot.js so
  the tools package can use them without browser globals.
- docs/CURRENT-STATE.md, changelog and comments updated; redundant tests
  trimmed.
This commit is contained in:
German Khokhlov committed 2026-10-30 13:06:53 +04:00
1 parent 54750a01e5
commit f967218a04
10 files changed
+232 -80

No files matched your search

+23 -5
View File
@@ -1,7 +1,11 @@
/** Composite queries that combine other tools' answers for one area. */
import { suggestView } from '../views.js';
import { feedProvenanceEnvelope } from '../../data/layerSnapshot.js';
import {
feedProvenanceEnvelope,
hudSummaryMatchesProvenance,
provenanceTag,
} from '../../data/layerSnapshot.js';
import { defineTool, ToolError } from '../catalog.js';
import {
AREA_SCHEMA,
@@ -232,6 +236,7 @@ export const getHudCaption = defineTool({
feedState: sectionFeedState(brief[key]),
}),
);
const feedProvenance = feedProvenanceEnvelope(layers);
const response = await context.services.summary.summarize(
{
placeLabels: [area.label],
@@ -244,16 +249,29 @@ export const getHudCaption = defineTool({
name,
feedState,
})),
feedProvenance: feedProvenanceEnvelope(layers),
feedProvenance,
},
{ signal: context.signal },
);
const caption = response?.data?.summary;
if (!response?.ok || typeof caption !== 'string' || !caption)
const words = response?.data?.summary;
if (!response?.ok || typeof words !== 'string' || !words)
throw new ToolError('unavailable', 'The caption service did not answer');
// As on the HUD: the warning is the app's own state tag, and a reply that
// names another state than the app's keeps only the place.
const tag = provenanceTag(feedProvenance);
const text = hudSummaryMatchesProvenance(response.data, feedProvenance)
? words
: area.label;
const caption = tag
? `${text} | ${[tag.state, ...tag.names].join(' ').toUpperCase()}`
: text;
return {
summary: caption,
data: { area: area.label, caption },
data: {
area: area.label,
caption,
feedState: feedProvenance.overall,
},
};
},
});
@@ -0,0 +1,66 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeCatalog, coreTools } from '../index.js';
// The proxy returns the feed state as its own field ({ summary, feedState });
// the five words no longer carry STALE, so get_hud_caption adds the app's own
// state tag and keeps only the place when the reply names another state.
async function staleCaption(reply) {
const conditions = {
observedAt: '2026-10-01T19:15:00.000Z',
temperatureC: 26.3,
apparentTemperatureC: 32,
precipitationMm: 0,
cloudCoverPct: 100,
windKph: 5.4,
windDirectionDeg: 318,
visibilityM: 15400,
weatherCode: 3,
};
const places = {
resolve: async (name) =>
name === 'Austin'
? {
name: 'Austin, Texas',
bounds: { west: -97.94, south: 30.1, east: -97.56, north: 30.52 },
}
: null,
};
const staleWeather = {
getConditions: async () => ({ status: 'stale', weather: conditions }),
};
let sent = null;
const summary = {
async summarize(context) {
sent = context;
return { ok: true, status: 200, data: { error: null, ...reply } };
},
};
const catalog = composeCatalog({
tools: coreTools,
services: { weather: staleWeather, summary, places },
});
const result = await catalog.call('get_hud_caption', {
area: { place: 'Austin' },
});
assert.equal(sent.feedProvenance.overall, 'stale');
return result.data;
}
test('get_hud_caption keeps the canonical feed state of a stale view', async () => {
const data = await staleCaption({
summary: 'Austin weather across the area',
feedState: 'stale',
});
assert.equal(data.feedState, 'stale');
assert.equal(data.caption, 'Austin weather across the area | STALE WEATHER');
});
test('get_hud_caption drops words whose feed state is not the app one', async () => {
const data = await staleCaption({
summary: 'Austin live weather looks fine',
feedState: 'nominal',
});
assert.equal(data.feedState, 'stale');
assert.equal(data.caption, 'Austin, Texas | STALE WEATHER');
});