fix(hud): state word first, layer names last; CJK cap by locale

Follow-up review fixes:

- The HUD line is STATE | text | LAYERS for the AI summary and the
  fallback line, so the narrow minimal and cyber layouts show both the
  warning and the summary. get_hud_caption builds its caption the same
  way. The unused "{state} {layers}" key is pruned from the catalogs.
- The 20-character cap applies only when the request locale is Japanese
  or Chinese; other languages keep five words around a Han place name.
- enabledLayers names are localized too, and the instructions name layers
  only from enabledLayerLabels, never from feedProvenance or its note.
- The keyless test restores GEV_RATELIMIT_OPENAI_PER_MIN again; comments
  and docs updated.
This commit is contained in:
German Khokhlov committed 2026-10-30 14:27:59 +04:00
1 parent f967218a04
commit 7b223d7d7b
17 files changed
+80 -51

No files matched your search

+21 -20
View File
@@ -104,22 +104,21 @@ const FEED_STATE_LABELS = Object.freeze({
});
/**
* The summary's feed-state suffix in the active language, e.g.
* `UNAVAILABLE: LIVE AIS VESSELS`. State ids and layer names stay English in
* layer data; only this line translates them.
* A summary line with its feed-state warning in the active language, e.g.
* `UNAVAILABLE | <text> | LIVE AIS VESSELS`. The state word leads so the
* one-line readout never clips it; the layer names trail so they never push
* the text out. State ids and layer names stay English in layer data; only
* this line translates them.
*/
function localizedProvenanceTag(provenance) {
if (!provenance) return null;
function withProvenance(text, provenance) {
if (!provenance) return text;
const state = FEED_STATE_LABELS[provenance.state]
? t(FEED_STATE_LABELS[provenance.state])
: provenance.state.toUpperCase();
if (!provenance.names.length) return state;
return t('{state} {layers}', {
state,
layers: provenance.names
.map((name) => localizeLayerName(name).toUpperCase())
.join('/'),
});
const layers = provenance.names
.map((name) => localizeLayerName(name).toUpperCase())
.join('/');
return [state, text, layers].filter(Boolean).join(' | ');
}
/** The name shown for a _viewBand() id. */
@@ -764,8 +763,8 @@ export class IntelHUD {
LOCALIZED_LOCALITY,
);
const provenance = localizedProvenanceTag(
hudTelemetryProvenance(this._dataManager?.getAll?.() || []),
const provenance = hudTelemetryProvenance(
this._dataManager?.getAll?.() || [],
);
// ALT, SUN and ONA are sensor codes, as in the corner readouts.
const line = t(
@@ -782,8 +781,7 @@ export class IntelHUD {
zone: localTag,
},
);
// The feed state leads: the one-line readout clips its end.
return provenance ? `${provenance} | ${line}` : line;
return withProvenance(line, provenance);
}
/** Stop a summary still typing out, leaving what it has typed so far. */
@@ -895,12 +893,11 @@ export class IntelHUD {
throw new Error(data?.error || `HTTP ${response.status}`);
}
if (hudSummaryMatchesProvenance(data, context.feedProvenance)) {
// The warning is the HUD's own translated tag, not the model's
// words, and it leads so the one-line readout never clips it.
const tag = localizedProvenanceTag(
// The warning is the HUD's own translated state, not the model's words.
this._lastSummaryText = withProvenance(
data.summary,
provenanceTag(context.feedProvenance),
);
this._lastSummaryText = tag ? `${tag} | ${data.summary}` : data.summary;
}
this._setSummaryText(this._lastSummaryText || fallbackText, animate);
} catch (error) {
@@ -973,6 +970,10 @@ export class IntelHUD {
// The names the user sees; feedProvenance keeps the English ids.
enabledLayerLabels:
layerContext.enabledLayerLabels.map(localizeLayerName),
enabledLayers: layerContext.enabledLayers.map((layer) => ({
...layer,
name: localizeLayerName(layer.name),
})),
};
}
+9 -3
View File
@@ -59,6 +59,7 @@ export const HUD_SUMMARY_INSTRUCTIONS = [
"Write one concise intelligence-HUD summary for God's Eye View.",
'Use only the supplied place, street, nearby-place, enabled-layer labels, and feedProvenance.',
'Prefer the clearest named place and include a relevant enabled layer only when useful.',
'Name layers only as in enabledLayerLabels; never copy layer names or state words from feedProvenance or its note.',
'Set feedState to feedProvenance.overall exactly as given, or none when it is null.',
'If feedProvenance.overall is partial, do not present the coverage as complete; if it is any other state but nominal, do not present the view as live or healthy.',
'Do not infer from coordinates or invent a place or feed state.',
@@ -95,7 +96,6 @@ export function hudSummaryLocale(value) {
return normalizeLocale(value) || DEFAULT_LOCALE;
}
/** The language a request's summary is written in, named in English. */
// Names Intl.DisplayNames gets wrong for the shipped catalogs: pt is the
// Brazilian variant, and zh-Hant is the zh-CN wording in Traditional script.
const SUMMARY_LANGUAGES = Object.freeze({
@@ -107,6 +107,12 @@ const SUMMARY_LANGUAGES = Object.freeze({
/** Scripts written without spaces, where five words do not apply. */
const UNSPACED_LOCALES = /^(?:ja|zh)(?:-|$)/;
/** Whether the summary for this locale is capped in characters, not words. */
export function hudSummaryIsUnspaced(locale) {
return UNSPACED_LOCALES.test(hudSummaryLocale(locale));
}
/** The language a request's summary is written in, named in English. */
function summaryLanguage(tag) {
if (SUMMARY_LANGUAGES[tag]) return SUMMARY_LANGUAGES[tag];
try {
@@ -125,7 +131,7 @@ export function hudSummaryInstructions(locale) {
const tag = hudSummaryLocale(locale);
if (tag === DEFAULT_LOCALE) return HUD_SUMMARY_INSTRUCTIONS;
const language = summaryLanguage(tag);
const length = UNSPACED_LOCALES.test(tag)
const length = hudSummaryIsUnspaced(tag)
? ` Instead of five words, write one phrase of at most ${HUD_SUMMARY_MAX_UNSPACED_CHARS} characters.`
: '';
return `${HUD_SUMMARY_INSTRUCTIONS} Write the summary in ${language} (${tag}), not English, giving places their usual ${language} names and layers the names in enabledLayerLabels; feedState stays the English value.${length}`;
@@ -172,7 +178,7 @@ export function hudSummaryLayerContext(layers = [], options) {
}
/**
* The feed-state tag for the HUD telemetry suffix when an enabled feed is not
* The warning for the HUD telemetry line when an enabled feed is not
* nominal.
* @param {Array<object>} [layers]
* @param {{ now?: number }} [options]
+30 -3
View File
@@ -164,6 +164,9 @@ test('the installed keyless HUD route stays successful after the voice quota is
} finally {
if (previousKey === undefined) delete process.env.OPENAI_API_KEY;
else process.env.OPENAI_API_KEY = previousKey;
if (previousLimit === undefined)
delete process.env.GEV_RATELIMIT_OPENAI_PER_MIN;
else process.env.GEV_RATELIMIT_OPENAI_PER_MIN = previousLimit;
}
});
@@ -263,10 +266,20 @@ test('the HUD sends its UI locale inside the context it signs for dedupe', () =>
);
});
test('the state tag leads the one-line summary so clipping cannot hide it', () => {
test('the state word leads and the layer names trail the one-line summary', () => {
const hud = readFileSync(new URL('./hud.js', import.meta.url), 'utf8');
assert.match(hud, /tag \? `\$\{tag\} \| \$\{data\.summary\}`/);
assert.match(hud, /provenance \? `\$\{provenance\} \| \$\{line\}`/);
assert.match(
hud,
/return \[state, text, layers\]\.filter\(Boolean\)\.join\(' \| '\);/,
);
assert.match(hud, /withProvenance\(\s*data\.summary,/);
assert.match(hud, /return withProvenance\(line, provenance\);/);
// The model sees only the layer names the user sees.
assert.match(hud, /name: localizeLayerName\(layer\.name\)/);
assert.match(
HUD_SUMMARY_INSTRUCTIONS,
/Name layers only as in enabledLayerLabels/,
);
});
test('shipped catalogs get their exact language, and CJK a character limit', () => {
@@ -370,6 +383,20 @@ test('a Japanese summary is capped in characters, not words', async () => {
);
});
for (const [locale, summary] of [
['en', 'Shibuya 渋谷 live flights overhead'],
['es', 'Vuelos sobre 東京駅 datos antiguos'],
['ru', 'Башня Тайбэй 101 台北101 рейсы'],
]) {
test(`a ${locale} summary with a Han place name keeps its five words`, async () => {
const { response } = await postHudSummary(
{ locale, feedProvenance: { overall: 'stale' } },
JSON.stringify({ summary, feedState: 'stale' }),
);
assert.equal(response.body.summary, summary);
});
}
test('a reply that skips the format carries no summary and no state', async () => {
const { response } = await postHudSummary(
{ locale: 'ru', feedProvenance: { overall: 'stale' } },
-1
View File
@@ -1125,7 +1125,6 @@
"OCEANIA": "OZEANIEN",
"NORTHERN OCEANIC GRID": "NÖRDLICHES OZEANRASTER",
"SOUTHERN OCEANIC GRID": "SÜDLICHES OZEANRASTER",
"{state} {layers}": "{state} {layers}",
"view band|STREET": "STRASSE",
"view band|CITY": "STADT",
"view band|METRO": "GROSSRAUM",
-1
View File
@@ -2025,7 +2025,6 @@
"Globe field image unavailable": "Imagen del campo en el globo no disponible",
"status chip|UNAVAILABLE": "NO DISP.",
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Contiene referencias a contenido registrado o medios vinculados. Esos archivos externos no se incluyen en un paquete de escena. Sus avisos originales siguen vigentes.",
"{state} {layers}": "{state}: {layers}",
"Selected Space Mission": "Misión espacial seleccionada",
"{duration}s + {hold}s": "{duration} s + {hold} s",
"Configured Source": "Fuente configurada",
-1
View File
@@ -332,7 +332,6 @@
"STALE": "OBSOLÈTE",
"PARTIAL": "PARTIEL",
"FALLBACK": "SECOURS",
"{state} {layers}": "{state} : {layers}",
"view band|STREET": "RUE",
"view band|CITY": "VILLE",
"view band|METRO": "MÉTROPOLE",
-1
View File
@@ -332,7 +332,6 @@
"STALE": "OBSOLETO",
"PARTIAL": "PARZIALE",
"FALLBACK": "RISERVA",
"{state} {layers}": "{state} {layers}",
"view band|STREET": "STRADA",
"view band|CITY": "CITTÀ",
"view band|METRO": "METRO",
-1
View File
@@ -326,7 +326,6 @@
"STALE": "更新遅延",
"PARTIAL": "部分的",
"FALLBACK": "フォールバック",
"{state} {layers}": "{state} {layers}",
"view band|STREET": "街路",
"view band|CITY": "市街",
"view band|METRO": "都市圏",
-1
View File
@@ -332,7 +332,6 @@
"STALE": "DESATUALIZADO",
"PARTIAL": "PARCIAL",
"FALLBACK": "ALTERNATIVO",
"{state} {layers}": "{state}: {layers}",
"view band|STREET": "RUA",
"view band|CITY": "CIDADE",
"view band|METRO": "METRÓPOLE",
-1
View File
@@ -2059,7 +2059,6 @@
"Globe field image unavailable": "Изображение поля на глобусе недоступно",
"status chip|UNAVAILABLE": "НЕДОСТ.",
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Используется зарегистрированный контент или связанные медиафайлы. Эти внешние файлы не входят в архив сцены. Их исходные условия использования по-прежнему действуют.",
"{state} {layers}": "{state}: {layers}",
"Selected Space Mission": "Выбранная космическая миссия",
"{duration}s + {hold}s": "{duration} с + {hold} с",
"Configured Source": "Настроенный источник",
-1
View File
@@ -335,7 +335,6 @@
"STALE": "ЗАСТАРІЛО",
"PARTIAL": "ЧАСТКОВО",
"FALLBACK": "РЕЗЕРВ",
"{state} {layers}": "{state}: {layers}",
"view band|STREET": "ВУЛИЦЯ",
"view band|CITY": "МІСТО",
"view band|METRO": "АГЛОМЕРАЦІЯ",
-1
View File
@@ -326,7 +326,6 @@
"STALE": "过期",
"PARTIAL": "部分",
"FALLBACK": "备用",
"{state} {layers}": "{state} {layers}",
"view band|STREET": "街道",
"view band|CITY": "城市",
"view band|METRO": "都市圈",
-1
View File
@@ -326,7 +326,6 @@
"STALE": "過期",
"PARTIAL": "部分",
"FALLBACK": "備用",
"{state} {layers}": "{state} {layers}",
"view band|STREET": "街道",
"view band|CITY": "城市",
"view band|METRO": "都市圈",
+5 -3
View File
@@ -256,14 +256,16 @@ export const getHudCaption = defineTool({
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.
// As on the HUD: the app's own state leads and its layers trail, 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()}`
? [tag.state.toUpperCase(), text, tag.names.join('/').toUpperCase()]
.filter(Boolean)
.join(' | ')
: text;
return {
summary: caption,
@@ -2,9 +2,7 @@ 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.
// get_hud_caption shows the app's own feed state, not the model's words.
async function staleCaption(reply) {
const conditions = {
observedAt: '2026-10-01T19:15:00.000Z',
@@ -53,7 +51,10 @@ test('get_hud_caption keeps the canonical feed state of a stale view', async ()
feedState: 'stale',
});
assert.equal(data.feedState, 'stale');
assert.equal(data.caption, 'Austin weather across the area | STALE WEATHER');
assert.equal(
data.caption,
'STALE | Austin weather across the area | WEATHER',
);
});
test('get_hud_caption drops words whose feed state is not the app one', async () => {
@@ -62,5 +63,5 @@ test('get_hud_caption drops words whose feed state is not the app one', async ()
feedState: 'nominal',
});
assert.equal(data.feedState, 'stale');
assert.equal(data.caption, 'Austin, Texas | STALE WEATHER');
assert.equal(data.caption, 'STALE | Austin, Texas | WEATHER');
});