auto-load extension i18n catalogs; add the missing tab-title ones

Five builtins (tickets, decisions, git, pql, problems) declared a
localized tab title but shipped no catalog and weren't in the hand-kept
preload list, so each logged "namespace not registered" on boot.
ExtensionManager now loads the i18n namespace of every localized
TabContribution when its extension activates — no manual list edit for a
new tab — and the five missing en_US catalogs are added.

T-155.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 16:49:56 +02:00
co-authored by Claude Opus 4.7
parent e5fa6302d3
commit 2fd911d01d
11 changed files with 91 additions and 1 deletions
@@ -2208,3 +2208,6 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-152', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 10:37:57', '2026-05-23 10:37:57', '2026-05-23 10:37:57', NULL, 'e12bfc4f8896514fb13450e0f83cf6c6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-152', 'status', 'in_progress', 'done', NULL, '2026-05-23 10:43:52', '2026-05-23 10:43:52', '2026-05-23 10:43:52', NULL, 'edab8c3daaabe51797afbe70b5f9bfde', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-156', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 11:30:35', '2026-05-23 11:30:35', '2026-05-23 11:30:35', NULL, '66000ad896a8ccd2ec91d2ae6c539f01', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-156', 'status', 'in_progress', 'done', NULL, '2026-05-23 12:05:19', '2026-05-23 12:05:19', '2026-05-23 12:05:19', NULL, '24db72624201c89a5c0bcb3b11840275', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-154', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:40:01', '2026-05-23 14:40:01', '2026-05-23 14:40:01', NULL, 'aff6dbb3095efd46cf786a53d8622d42', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-155', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:40:05', '2026-05-23 14:40:05', '2026-05-23 14:40:05', NULL, 'e46f5eba5bd678190954744c2f8338bc', 1) ON CONFLICT(hash) DO NOTHING;
+15
View File
@@ -2607,3 +2607,18 @@ Tension to respect: T-146 deliberately pinned each pane to its own --session-id
Direction (needs a small decision/spike): detect when Claude switches the active session for a pane and re-bind the TranscriptReader to the new session-id without re-introducing the T-146 cross-pane bug e.g. via the transcript''s session lineage (parentUuid/leaf, or a session-change marker), or by querying the active session for the pane''s tmux session. Near-term safety to weigh: detect session-mutating commands (/clear, /compact, /resume) and handle/warn rather than silently wedging. Immediate user workaround: restart clide (primary re-binds + --resume to the pre-clear session).
Acceptance: after a /clear in a Claude pane, clide follows to the new session and continues to show prompts + responses; secondaries still bind only to their own session (T-146 not regressed); tests cover a simulated session switch re-binding the reader; coverage >= floor.', 'in_progress', 'high', NULL, NULL, 'D-75', '2026-05-23 11:15:08', '2026-05-23 11:30:35', NULL, '8a1383c7726d30afc1336d47b6a4984c', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-156', 'bug', 'T-132', 'Claude pane doesn''t follow session switches (/clear, /compact) — interaction wedges', 'Now that T-153 makes slash commands actually fire, session-mutating commands wedge the Claude pane. /clear makes Claude Code start a NEW session-id (a fresh <uuid>.jsonl). The composer''s keystrokes still reach the same tmux/claude process, but clide''s TranscriptReader is pinned to the spawn --session-id file (claude_pane binds the reader to a fixed transcriptFile; transcript_reader.dart has zero session-change handling). After /clear: the old transcript freezes, Claude writes to the new session file, and clide keeps tailing the dead file — no responses appear and the pane looks completely broken. Affects /clear and likely /compact, /resume, --fork-session.
Observed (2026-05-23): a new <uuid>.jsonl (8b21f12b) appeared at 13:11 while the prior froze at 13:07, matching a /clear at that time. Pre-T-153 this was hidden because slash commands were pasted as literal text (no-op).
Tension to respect: T-146 deliberately pinned each pane to its own --session-id to stop secondaries rendering the primary''s transcript (the older newest-jsonl approach crossed panes). So the fix must NOT just follow the newest jsonl globally — it must follow THIS pane''s session lineage when Claude forks it.
Direction (needs a small decision/spike): detect when Claude switches the active session for a pane and re-bind the TranscriptReader to the new session-id without re-introducing the T-146 cross-pane bug e.g. via the transcript''s session lineage (parentUuid/leaf, or a session-change marker), or by querying the active session for the pane''s tmux session. Near-term safety to weigh: detect session-mutating commands (/clear, /compact, /resume) and handle/warn rather than silently wedging. Immediate user workaround: restart clide (primary re-binds + --resume to the pre-clear session).
Acceptance: after a /clear in a Claude pane, clide follows to the new session and continues to show prompts + responses; secondaries still bind only to their own session (T-146 not regressed); tests cover a simulated session switch re-binding the reader; coverage >= floor.', 'done', 'high', NULL, NULL, 'D-75', '2026-05-23 11:15:08', '2026-05-23 12:05:19', NULL, '35311a7e0ad2473412db12bb0c82a4e7', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-154', 'task', 'T-132', 'Surface ClaudeConfig in the Claude status pane', 'The status surface shows LIVE model / permission-mode / context-tokens from the active transcript (T-145, T-150). Complement it with the CONFIGURED side from ClaudeConfig (T-151): available-skills count and/or configured permission/model defaults — static environment state alongside live session state. Reads from ClaudeConfig, not the filesystem; updates when config changes. Acceptance: the status surface reflects ClaudeConfig values, refreshes on config change, and keeps the live transcript-driven fields working; widget test; coverage >= floor. Blocked by T-151.', 'in_progress', 'low', NULL, NULL, 'D-76', '2026-05-23 09:52:32', '2026-05-23 14:40:01', NULL, 'e375208758cf11ba342c0c3cb88f1767', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-155', 'task', NULL, 'Finish i18n catalog plumbing: auto-load extension namespaces, kill ''namespace not registered'' warnings', 'Five builtins (tickets, decisions, git, pql, problems) declare titleKey: ''tab.title'' + i18nNamespace on their TabContribution but ship NO catalog file (lib/kernel/src/i18n/catalog/ has only the 8 tier-0 namespaces) and aren''t in the hand-maintained _tier0Namespaces preload list (lib/main.dart). Result: every boot logs ''i18n: namespace not registered: builtin.X (key: tab.title)'' for each. Tabs still render correctly via the placeholder fallback, so it''s cosmetic console noise — but it recurs for every new builtin because the preload list is maintained by hand.
Proper fix: when an extension activates, auto-load the i18nNamespace(s) declared by its contributions via i18n.ensureNamespaceLoaded removing the manual _tier0Namespaces list and the drift it causes. AssetCatalogLoader.load returns empty for a missing file, so auto-preloading alone would just shift the warning to ''missing key tab.title'' so the missing catalogs must also be created: builtin.{tickets,decisions,git,pql,problems}_en_us.json with at least tab.title plus any other keys those extensions look up.
Acceptance: clean boot with all builtins active logs zero i18n warnings; a new builtin that declares a titleKey gets its namespace loaded automatically with no manual list edit; the five catalog files exist; tests cover the auto-load-on-activate path and the missing-catalog case; coverage >= floor.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-23 11:06:50', '2026-05-23 14:40:05', NULL, 'a26532f49dab8052e03654393747ce63', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+4
View File
@@ -136,6 +136,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Fixed
- The git / tickets / decisions / pql / problems tabs no longer log
`i18n: namespace not registered` on boot (T-155). An extension's
localized tab title is now loaded automatically on activation, and the
five missing catalogs were added.
- Slash commands sent from the Claude composer now actually run (T-153).
Recognised commands are delivered as typed input so Claude's TUI parses
them; other input (and stray leading slashes like a `/tmp` path) stays
+10
View File
@@ -146,6 +146,16 @@ class ExtensionManager extends ChangeNotifier {
for (final c in ext.contributions) {
_applyContribution(c);
}
// Eagerly load the i18n catalog for any localized tab this extension
// contributes, so its title resolves without a "namespace not
// registered" warning — and without the namespace having to be listed
// by hand at boot (T-155).
for (final ns in {
for (final c in ext.contributions)
if (c is TabContribution && c.i18nNamespace != null) c.i18nNamespace!,
}) {
await i18n.ensureNamespaceLoaded(ns);
}
_activated.add(id);
_failed.remove(id);
events.emit(ExtensionActivated(id: id));
@@ -0,0 +1,3 @@
{
"tab.title": { "translation": "Decisions" }
}
@@ -0,0 +1,3 @@
{
"tab.title": { "translation": "Git" }
}
@@ -0,0 +1,3 @@
{
"tab.title": { "translation": "pql" }
}
@@ -0,0 +1,3 @@
{
"tab.title": { "translation": "Problems" }
}
@@ -0,0 +1,3 @@
{
"tab.title": { "translation": "Tickets" }
}
+2 -1
View File
@@ -18,6 +18,7 @@ class KernelFixture {
static Future<KernelFixture> create({
List<ThemeDefinition>? bundledThemes,
Map<String, Map<Locale, Map<String, Object?>>>? i18nCatalogs,
List<String>? preloadNamespaces,
Locale? initialLocale,
Locale defaultLocale = const Locale('en', 'US'),
}) async {
@@ -29,7 +30,7 @@ class KernelFixture {
appDir: tempDir,
bundledThemes: themes,
i18nLoader: InMemoryCatalogLoader(catalogs),
preloadNamespaces: catalogs.keys.toList(),
preloadNamespaces: preloadNamespaces ?? catalogs.keys.toList(),
defaultLocale: defaultLocale,
initialLocale: initialLocale,
daemonClientFactory: (log, events, _) {
@@ -105,6 +105,48 @@ void main() {
);
});
test('activating an extension auto-loads its localized tab namespace (T-155)', () async {
final local = await KernelFixture.create(
i18nCatalogs: {
'ext.localized': {
const Locale('en', 'US'): {
'tab.title': {'translation': 'Localized'},
},
},
},
preloadNamespaces: const [], // loadable, but not preloaded at boot
);
addTearDown(local.dispose);
// Not registered yet → string() falls back to the placeholder.
expect(
local.services.i18n.string('tab.title', namespace: 'ext.localized', placeholder: 'fallback'),
'fallback',
);
local.services.extensions.register(_Ext(
id: 'ext.localized',
contributions: [
TabContribution(
id: 'ext.localized.view',
slot: Slots.workspace,
title: 'Fallback',
titleKey: 'tab.title',
i18nNamespace: 'ext.localized',
build: (_) => const SizedBox.shrink(),
),
],
));
await local.services.extensions.activateAll();
// Activation auto-loaded the catalog → resolves, no "namespace not
// registered" warning.
expect(
local.services.i18n.string('tab.title', namespace: 'ext.localized', placeholder: 'fallback'),
'Localized',
);
});
test('deactivate removes contributions from the registry', () async {
f.services.extensions.register(_Ext(
id: 'ephemeral',