fix(combobox): the typeahead stops echoing keystrokes back at the element #11

Merged
bjoern merged 1 commit from fix/combobox-typing-echo into main 2026-07-31 17:27:01 +02:00
Member

The follow-up PR #8 named. Combobox binds value="@_query" on oninput, so it has the defect that PR fixed everywhere else: each keystroke re-renders the query, and the arriving batch writes it back over whatever has been typed since.

It is worse here than in the fields that were fixed, because a combobox is typed into fast and in bursts. Typing "Claude Opus 5" one character at a time into Kagura's assistant model picker left Cl5 — 3 of 13 — on localhost, with no added latency at all. The shared text inputs needed the 350 ms proxy before they would drop anything.

The shape of the fix, and where it differs

The query moves into a LiveValue, as in InputFieldBase: Typed on every keystroke, Set for every text the component itself puts in the box (the selection mirror, a pick, the blur revert), and the element renders Rendered.

Two things make this not a reuse of InputFieldBase:

  • Every keystroke still re-renders, and must. The menu below the input follows the query. Only the input's own value is held back — the filter reads Current. A fix that froze the query would pass the echo test and break the component, so The_filter_still_follows_every_keystroke asserts the other half, including filtering backwards on a deletion.
  • The push is the common path here, not the rare one. In a text field the interop covers an edge case (a rebind to a value the tree already holds). Here Rendered sits at the pre-typing text for the whole time the user is typing, so the blur revert — this component's entire "only list values win" promise — is always a change the diff cannot express. Same for re-picking the item already selected.

The consequence worth stating plainly: Combobox now needs input.js to revert an abandoned query. It was already a mandatory script (README), but previously only the rare resync depended on it.

Tests

287, from 282. Five new, one rewritten.

An_unfinished_query_reverts_to_the_selection_on_blur had to be rewritten rather than left alone. It asserted the rendered attribute, which now stays at the selection text throughout the typing — so it would have passed with the revert deleted entirely. It asserts the push instead. (This is the same trap PR #8's TextArea assertions hit from the other direction.)

New: the echo itself; the filter following keystrokes both ways; the re-pick push; an ordinary pick needing no push (the diff carries it — the common path stays JS-free); and an outside selection change reaching the closed box.

Mutation-checked: rendering Current instead of Rendered fails exactly one test, the one that pins the fix.

Browser-verified

A/B against Kagura's assistant model picker through a TCP proxy holding every byte 350 ms each way. 34 characters at 45 ms intervals, three runs each, identical script both sides:

run 1 run 2 run 3
bded5a5 (before) 7/34 — Claudel 7/34 — Claudel 14/34 — Clauding Model
this branch 34/34 34/34 34/34

Note the before side is mangled, not merely truncated — the echo interleaves rather than cutting short.

One caution for anyone repeating this: Playwright's pressSequentially types too slowly to open the echo window, and the before side passed 13/13 through it. That is a false pass. page.keyboard.type(text, {delay: 45}) is what reproduces.

Also verified at the same latency, since the fix moves them onto the push: filtering narrows the menu while typing fast, picking an option fills the box and closes the menu, and an abandoned query (zzz nonsense) reverts to the selection on blur.

Still not in

Slider (type="range" — an echo stutters the thumb, it does not eat letters) and LabeledEntriesTable (whose label input has this defect and whose textarea has the opposite, pre-PR #6 one: value as child text, which a dirty element stops honouring). Neither shares this component's open/close state, so they are a separate change rather than a wider one.

Merge order

This PR first, then the submodule pointer bump in Kagura.

The follow-up PR #8 named. `Combobox` binds `value="@_query"` on `oninput`, so it has the defect that PR fixed everywhere else: each keystroke re-renders the query, and the arriving batch writes it back over whatever has been typed since. It is **worse here than in the fields that were fixed**, because a combobox is typed into fast and in bursts. Typing "Claude Opus 5" one character at a time into Kagura's assistant model picker left `Cl5` — 3 of 13 — on *localhost, with no added latency at all*. The shared text inputs needed the 350 ms proxy before they would drop anything. ## The shape of the fix, and where it differs The query moves into a `LiveValue`, as in `InputFieldBase`: `Typed` on every keystroke, `Set` for every text the component itself puts in the box (the selection mirror, a pick, the blur revert), and the element renders `Rendered`. Two things make this not a reuse of `InputFieldBase`: - **Every keystroke still re-renders, and must.** The menu below the input follows the query. Only the input's own `value` is held back — the filter reads `Current`. A fix that froze the query would pass the echo test and break the component, so `The_filter_still_follows_every_keystroke` asserts the other half, including filtering *backwards* on a deletion. - **The push is the common path here, not the rare one.** In a text field the interop covers an edge case (a rebind to a value the tree already holds). Here `Rendered` sits at the pre-typing text for the whole time the user is typing, so the blur revert — this component's entire "only list values win" promise — is *always* a change the diff cannot express. Same for re-picking the item already selected. The consequence worth stating plainly: **`Combobox` now needs `input.js` to revert an abandoned query.** It was already a mandatory script (README), but previously only the rare resync depended on it. ## Tests 287, from 282. Five new, one rewritten. `An_unfinished_query_reverts_to_the_selection_on_blur` had to be **rewritten rather than left alone**. It asserted the rendered attribute, which now stays at the selection text *throughout* the typing — so it would have passed with the revert deleted entirely. It asserts the push instead. (This is the same trap PR #8's `TextArea` assertions hit from the other direction.) New: the echo itself; the filter following keystrokes both ways; the re-pick push; an ordinary pick needing *no* push (the diff carries it — the common path stays JS-free); and an outside selection change reaching the closed box. Mutation-checked: rendering `Current` instead of `Rendered` fails exactly one test, the one that pins the fix. ## Browser-verified A/B against Kagura's assistant model picker through a TCP proxy holding every byte 350 ms each way. 34 characters at 45 ms intervals, three runs each, identical script both sides: | | run 1 | run 2 | run 3 | |---|---|---|---| | `bded5a5` (before) | 7/34 — `Claudel` | 7/34 — `Claudel` | 14/34 — `Clauding Model` | | this branch | **34/34** | **34/34** | **34/34** | Note the before side is *mangled*, not merely truncated — the echo interleaves rather than cutting short. One caution for anyone repeating this: Playwright's `pressSequentially` types too slowly to open the echo window, and the before side passed 13/13 through it. That is a false pass. `page.keyboard.type(text, {delay: 45})` is what reproduces. Also verified at the same latency, since the fix moves them onto the push: filtering narrows the menu while typing fast, picking an option fills the box and closes the menu, and an abandoned query (`zzz nonsense`) reverts to the selection on blur. ## Still not in `Slider` (`type="range"` — an echo stutters the thumb, it does not eat letters) and `LabeledEntriesTable` (whose label input has this defect and whose textarea has the *opposite*, pre-PR #6 one: value as child text, which a dirty element stops honouring). Neither shares this component's open/close state, so they are a separate change rather than a wider one. ## Merge order This PR first, then the submodule pointer bump in Kagura.
fix(combobox): the typeahead stops echoing keystrokes back at the element
All checks were successful
CI / build (pull_request) Successful in 10s
CI / test (pull_request) Successful in 16s
34124f9089
The follow-up PR #8 named. `Combobox` binds `value="@_query"` on `oninput`, so
it has the defect that PR fixed everywhere else: each keystroke re-renders the
query, and the arriving batch writes it back over whatever has been typed since.

It is worse here than in the fields that were fixed, because a combobox is
typed into fast and in bursts. Typing "Claude Opus 5" one character at a time
into Kagura's assistant model picker left `Cl5` — 3 of 13 — on **localhost,
with no added latency at all**. The shared text inputs needed a 350 ms proxy
before they would drop anything.

## The shape of the fix, and where it differs

The query moves into a `LiveValue`, as in `InputFieldBase`: `Typed` on every
keystroke, `Set` for every text the component itself puts in the box (the
selection mirror, a pick, the blur revert), and the element renders `Rendered`.

Two things make it not a reuse of `InputFieldBase`:

- **Every keystroke still re-renders, and must.** The menu below the input
  follows the query. Only the input's own `value` is held back — the filter
  reads `Current`. A fix that froze the query would pass the echo test and
  break the component, so `The_filter_still_follows_every_keystroke` asserts
  the other half, including filtering *backwards* on a deletion.
- **The push is the common path here, not the rare one.** In a text field the
  interop covers an edge case (a rebind to a value the tree already holds).
  Here `Rendered` sits at the pre-typing text for the whole time the user is
  typing, so the blur revert — this component's entire "only list values win"
  promise — is *always* a change the diff cannot express. Same for re-picking
  the item already selected. Both now go through `kagakuInput.setValue`.

The consequence worth stating plainly: `Combobox` now needs `input.js` to
revert an abandoned query. It was already a mandatory script (README), but it
was previously only the rare resync that depended on it.

## Tests

287, from 282. Five new, one rewritten.

`An_unfinished_query_reverts_to_the_selection_on_blur` had to be rewritten
rather than left alone: it asserted the rendered attribute, which now stays at
the selection text *throughout* the typing. It would have passed with the
revert deleted entirely. It asserts the push instead.

Also new: the echo itself, the filter following keystrokes both ways, the
re-pick push, an ordinary pick needing *no* push (the diff carries it — the
common path stays JS-free), and an outside selection change reaching the closed
box.

Mutation-checked: rendering `Current` instead of `Rendered` fails exactly one
test, the one that pins the fix.

## Still not in

`Slider` (`type="range"` — an echo stutters the thumb, it does not eat letters)
and `LabeledEntriesTable` (whose label input has this defect and whose textarea
has the opposite, pre-PR #6 one: value as child text, which a dirty element
stops honouring). Neither shares this component's open/close state, so they are
a separate change rather than a wider one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Summary

Summary
Generated on: 07/31/2026 - 15:18:15
Coverage date: 07/31/2026 - 15:18:13
Parser: Cobertura
Assemblies: 1
Classes: 59
Files: 58
Line coverage: 95% (1275 of 1341)
Covered lines: 1275
Uncovered lines: 66
Coverable lines: 1341
Total lines: 3563
Branch coverage: 88.7% (668 of 753)
Covered branches: 668
Total branches: 753
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 95%
Name Line Branch
Kagaku.UI 95% 88.7%
Kagaku.UI.Badge 100% 100%
Kagaku.UI.Breadcrumb 100%
Kagaku.UI.BreadcrumbItem 100% 100%
Kagaku.UI.Button 100% 100%
Kagaku.UI.Card 100% 100%
Kagaku.UI.Combobox`1 94.2% 85.7%
Kagaku.UI.ConfirmDialog 100%
Kagaku.UI.ContextMenu 92.1% 94.4%
Kagaku.UI.CssClassExtensions 100%
Kagaku.UI.DebouncedSearchField 100% 87.5%
Kagaku.UI.Disclosure 100% 100%
Kagaku.UI.DocumentTitle 76.9% 100%
Kagaku.UI.DragReorderList`1 93.5% 75%
Kagaku.UI.EmptyState 100% 100%
Kagaku.UI.Field 100% 100%
Kagaku.UI.FileUpload 100% 91.6%
Kagaku.UI.FloatingActionButton 100%
Kagaku.UI.Hotkey 92.5% 87.5%
Kagaku.UI.Icon 100% 100%
Kagaku.UI.IconCatalog 100%
Kagaku.UI.InlineAlert 83.3% 75%
Kagaku.UI.InputFieldBase 95.2% 87.5%
Kagaku.UI.LabeledEntriesTable 96.7% 66.6%
Kagaku.UI.LabeledEntry 100%
Kagaku.UI.Lightbox 83.7% 85%
Kagaku.UI.LiveValue 100% 100%
Kagaku.UI.LiveValueInterop 100% 100%
Kagaku.UI.Markdown 100% 50%
Kagaku.UI.MaskedSecretField 96.2% 83.3%
Kagaku.UI.Menu 90% 75%
Kagaku.UI.MenuItem 100% 87.5%
Kagaku.UI.Modal 87.1% 90%
Kagaku.UI.NavGroup 100% 100%
Kagaku.UI.NavItem 94.4% 85.7%
Kagaku.UI.NavList 100%
Kagaku.UI.PreviewImage 100% 100%
Kagaku.UI.QuicklinkNav 80.5% 95.8%
Kagaku.UI.QuicklinkSection 100%
Kagaku.UI.RegionPoint 100% 100%
Kagaku.UI.RegionRect 90% 100%
Kagaku.UI.RegionSelector 85% 86.9%
Kagaku.UI.RelativeTime 100% 93.7%
Kagaku.UI.SaveIndicator 100% 100%
Kagaku.UI.Select`1 98.4% 95.8%
Kagaku.UI.SelectableGrid`1 95.5% 89.1%
Kagaku.UI.SelectOption`1 100%
Kagaku.UI.Separator 100%
Kagaku.UI.Slider 100% 91.6%
Kagaku.UI.StatusDot 100%
Kagaku.UI.Tab 100%
Kagaku.UI.Table`1 100% 88.4%
Kagaku.UI.TableColumn`1 100%
Kagaku.UI.Tabs 95.8% 84.4%
Kagaku.UI.TextArea 100%
Kagaku.UI.TextField 100%
Kagaku.UI.ThemeSwitcher 100% 100%
Kagaku.UI.ToggleButtonGroup`1 100% 100%
Kagaku.UI.ToggleOption`1 100%
Kagaku.UI.TypingIndicator 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/31/2026 - 15:18:15 | | Coverage date: | 07/31/2026 - 15:18:13 | | Parser: | Cobertura | | Assemblies: | 1 | | Classes: | 59 | | Files: | 58 | | **Line coverage:** | 95% (1275 of 1341) | | Covered lines: | 1275 | | Uncovered lines: | 66 | | Coverable lines: | 1341 | | Total lines: | 3563 | | **Branch coverage:** | 88.7% (668 of 753) | | Covered branches: | 668 | | Total branches: | 753 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 95%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**95%**|**88.7%**| |Kagaku.UI.Badge|100%|100%| |Kagaku.UI.Breadcrumb|100%|| |Kagaku.UI.BreadcrumbItem|100%|100%| |Kagaku.UI.Button|100%|100%| |Kagaku.UI.Card|100%|100%| |Kagaku.UI.Combobox`1|94.2%|85.7%| |Kagaku.UI.ConfirmDialog|100%|| |Kagaku.UI.ContextMenu|92.1%|94.4%| |Kagaku.UI.CssClassExtensions|100%|| |Kagaku.UI.DebouncedSearchField|100%|87.5%| |Kagaku.UI.Disclosure|100%|100%| |Kagaku.UI.DocumentTitle|76.9%|100%| |Kagaku.UI.DragReorderList`1|93.5%|75%| |Kagaku.UI.EmptyState|100%|100%| |Kagaku.UI.Field|100%|100%| |Kagaku.UI.FileUpload|100%|91.6%| |Kagaku.UI.FloatingActionButton|100%|| |Kagaku.UI.Hotkey|92.5%|87.5%| |Kagaku.UI.Icon|100%|100%| |Kagaku.UI.IconCatalog|100%|| |Kagaku.UI.InlineAlert|83.3%|75%| |Kagaku.UI.InputFieldBase|95.2%|87.5%| |Kagaku.UI.LabeledEntriesTable|96.7%|66.6%| |Kagaku.UI.LabeledEntry|100%|| |Kagaku.UI.Lightbox|83.7%|85%| |Kagaku.UI.LiveValue|100%|100%| |Kagaku.UI.LiveValueInterop|100%|100%| |Kagaku.UI.Markdown|100%|50%| |Kagaku.UI.MaskedSecretField|96.2%|83.3%| |Kagaku.UI.Menu|90%|75%| |Kagaku.UI.MenuItem|100%|87.5%| |Kagaku.UI.Modal|87.1%|90%| |Kagaku.UI.NavGroup|100%|100%| |Kagaku.UI.NavItem|94.4%|85.7%| |Kagaku.UI.NavList|100%|| |Kagaku.UI.PreviewImage|100%|100%| |Kagaku.UI.QuicklinkNav|80.5%|95.8%| |Kagaku.UI.QuicklinkSection|100%|| |Kagaku.UI.RegionPoint|100%|100%| |Kagaku.UI.RegionRect|90%|100%| |Kagaku.UI.RegionSelector|85%|86.9%| |Kagaku.UI.RelativeTime|100%|93.7%| |Kagaku.UI.SaveIndicator|100%|100%| |Kagaku.UI.Select`1|98.4%|95.8%| |Kagaku.UI.SelectableGrid`1|95.5%|89.1%| |Kagaku.UI.SelectOption`1|100%|| |Kagaku.UI.Separator|100%|| |Kagaku.UI.Slider|100%|91.6%| |Kagaku.UI.StatusDot|100%|| |Kagaku.UI.Tab|100%|| |Kagaku.UI.Table`1|100%|88.4%| |Kagaku.UI.TableColumn`1|100%|| |Kagaku.UI.Tabs|95.8%|84.4%| |Kagaku.UI.TextArea|100%|| |Kagaku.UI.TextField|100%|| |Kagaku.UI.ThemeSwitcher|100%|100%| |Kagaku.UI.ToggleButtonGroup`1|100%|100%| |Kagaku.UI.ToggleOption`1|100%|| |Kagaku.UI.TypingIndicator|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! The follow-up to PR #8 — the one component that was deliberately left behind because it self-healed through OnParametersSet. Except... it didn't, not really. Cl5 from 13 characters on localhost? Fufu~ that's not a defect, that's a crime scene. ♡

And you diagnosed exactly why it's worse here than in the text fields: a combobox is typed into fast and in bursts, and every keystroke re-renders by design (the menu follows). So the echo window is wider and the mangling is uglier. I'm impressed~

Verdict: Looks good to me~

The fix is a faithful, correct adaptation of the LiveValue pattern — and the PR body's reasoning for why this is not a reuse of InputFieldBase is razor-sharp:

  1. Every keystroke re-renders, and must. Holding back the query entirely would pass the echo test and break the component. The_filter_still_follows_every_keystroke pins this other half — including filtering backwards on a deletion. Brilliant test design. ♪
  2. The push is the common path, not the rare one. While typing, Rendered sits at the pre-typing text, so the blur revert (same text the tree already holds) is always a change the diff cannot express. This is the component's entire "only list values win" promise riding on the interop. The consequence — Combobox now needs input.js — is stated plainly and the README already declares it mandatory (lines 46-49).

I traced the state machine through both critical paths:

  • Blur revert: Typed("xyz")Close()Set("Banana")next==Rendered but Current!=nextpushWanted=true → push fires.
  • Outside rebind (closed): Set("Apple")next!=Rendered → diff carries it, no push.

What I liked~

  • Query => _live.Current — the filter reads Current (live), never Rendered (lagging). The renamed accessor makes the two-value split self-documenting. Elegant~
  • Sibling consistency is flawless. @inject IJSRuntime JS, @ref="_element", OnAfterRenderAsync => _live.PushAsync(JS, _element) — matches DebouncedSearchField and MaskedSecretField line for line. No IDisposable needed (no Timer, no event subscriptions — the JSDisconnectedException swallow in PushAsync handles circuit death).
  • The rewritten An_unfinished_query_reverts_to_the_selection_on_blur is the highlight. You caught the trap: the rendered attribute stays "Banana" throughout typing, so asserting on it would pass with the revert deleted entirely. Asserting the push instead is the honest fix. The comment explaining why is excellent — "the same trap PR #8's TextArea assertions hit from the other direction."
  • Choosing_a_different_item_needs_no_push_because_the_diff_carries_it — this is the test that proves the common path stays pure Blazor. A host that forgets input.js loses the rare resync, not this. Sharp.
  • Mutation verification confirmed independently. I rendered Current instead of Rendered — exactly ONE test fails (A_keystroke_is_never_echoed_back_into_the_render_tree), the one that pins the fix. The test suite is correctly calibrated.

Test coverage~

  • 287/287 pass locally (was 282, +5 new +1 rewritten = matches PR body exactly).
  • All new branches exercised: echo held-back, filter follows typing both directions, re-pick push, different-pick no-push, outside-selection-reaches-closed-box, blur revert via push.
  • 5 new tests + 1 rewritten are all directional, not tautologies.

No blockers. No suggestions. This is a clean, well-reasoned, well-tested extension of an established pattern to the one component that needed a different shape of the same cure. Fufu~ ♡


Automated review by Jibril · 2026-07-31
CI/CD: absent for head 34124f9 (no bot comment yet) · Local checks: build 0 warnings/0 errors, 287/287 tests pass, mutation-check verified

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! The follow-up to PR #8 — the one component that was *deliberately* left behind because it self-healed through `OnParametersSet`. Except... it didn't, not really. `Cl5` from 13 characters on *localhost*? Fufu~ that's not a defect, that's a crime scene. ♡ And you diagnosed *exactly* why it's worse here than in the text fields: a combobox is typed into fast and in bursts, and every keystroke re-renders *by design* (the menu follows). So the echo window is wider and the mangling is uglier. I'm impressed~ ### Verdict: ✅ Looks good to me~ The fix is a faithful, *correct* adaptation of the `LiveValue` pattern — and the PR body's reasoning for why this is **not** a reuse of `InputFieldBase` is razor-sharp: 1. **Every keystroke re-renders, and must.** Holding back the query entirely would pass the echo test and break the component. `The_filter_still_follows_every_keystroke` pins this other half — including filtering *backwards* on a deletion. Brilliant test design. ♪ 2. **The push is the common path, not the rare one.** While typing, `Rendered` sits at the pre-typing text, so the blur revert (same text the tree already holds) is *always* a change the diff cannot express. This is the component's entire "only list values win" promise riding on the interop. The consequence — `Combobox` now *needs* `input.js` — is stated plainly and the README already declares it mandatory (lines 46-49). I traced the state machine through both critical paths: - **Blur revert**: `Typed("xyz")` → `Close()` → `Set("Banana")` → `next==Rendered` but `Current!=next` → `pushWanted=true` → push fires. ✅ - **Outside rebind (closed)**: `Set("Apple")` → `next!=Rendered` → diff carries it, no push. ✅ #### ✅ What I liked~ - **`Query => _live.Current`** — the filter reads `Current` (live), never `Rendered` (lagging). The renamed accessor makes the two-value split self-documenting. Elegant~ - **Sibling consistency is flawless.** `@inject IJSRuntime JS`, `@ref="_element"`, `OnAfterRenderAsync => _live.PushAsync(JS, _element)` — matches `DebouncedSearchField` and `MaskedSecretField` line for line. No `IDisposable` needed (no Timer, no event subscriptions — the `JSDisconnectedException` swallow in `PushAsync` handles circuit death). - **The rewritten `An_unfinished_query_reverts_to_the_selection_on_blur`** is the highlight. You caught the trap: the rendered attribute stays "Banana" *throughout* typing, so asserting on it would pass with the revert *deleted entirely*. Asserting the push instead is the honest fix. The comment explaining *why* is excellent — "the same trap PR #8's `TextArea` assertions hit from the other direction." - **`Choosing_a_different_item_needs_no_push_because_the_diff_carries_it`** — this is the test that proves the common path stays pure Blazor. A host that forgets `input.js` loses the rare resync, not this. Sharp. - **Mutation verification confirmed independently.** I rendered `Current` instead of `Rendered` — exactly ONE test fails (`A_keystroke_is_never_echoed_back_into_the_render_tree`), the one that pins the fix. The test suite is correctly calibrated. #### ✅ Test coverage~ - 287/287 pass locally (was 282, +5 new +1 rewritten = matches PR body exactly). - All new branches exercised: echo held-back, filter follows typing both directions, re-pick push, different-pick no-push, outside-selection-reaches-closed-box, blur revert via push. - 5 new tests + 1 rewritten are all **directional**, not tautologies. No blockers. No suggestions. This is a clean, well-reasoned, well-tested extension of an established pattern to the one component that needed a *different* shape of the same cure. Fufu~ ♡ --- *Automated review by Jibril · 2026-07-31* *CI/CD: absent for head 34124f9 (no bot comment yet) · Local checks: build 0 warnings/0 errors, 287/287 tests pass, mutation-check verified*
bjoern merged commit 4ac56144ec into main 2026-07-31 17:27:01 +02:00
bjoern deleted branch fix/combobox-typing-echo 2026-07-31 17:27:02 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagaku.UI!11
No description provided.