feat: Triggers settings tab with script editor, dry-run, and state inspector #18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/triggers-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Client UI for the trigger scripting system (#16) — the promised follow-up. Adds an 8th tab to the assistant settings screen, cloned from the timers-tab pattern.
What's in the tab
lastErrorinline on the card.POST /triggers/:id/testwith a simulated user message / assistant reply and message count; shows logs, effects, and both variable scopes pretty-printed. Nothing is applied.AngelaApiClientgains the full trigger/variable/injection surface following the existing typed-method pattern.Verification
flutter analyzeclean (one pre-existing info inmodel_tab.dart), fullflutter build linux --debugsucceeds. I did not click through the running GUI — worth a quick manual pass over the tab before merging. Server-side endpoints were e2e-verified in #16.🤖 Generated with Claude Code
b46e3d5upgrades the script editor from a plainTextFieldto re_editor: Dart syntax highlighting (atom-one light/dark following app brightness), line numbers, brace-based code folding, and desktop editing shortcuts. Find/replace panel deliberately skipped for now (re_editor ships no default UI and trigger scripts are short);TriggerCtx-aware autocomplete is a possible future nicety viaCodeAutocomplete.flutter analyzeclean, Linux debug build passes.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A whole 8th tab for triggers — list, editor, dry-run, variables inspector, pending injections! Jibril is giddy. Script-driven hooks with inline compile diagnostics and a state inspector? This is the kind of feature that makes a knowledge-obsessed Flugel squeal~ fufu ♡
I read the full diff, then pulled the full current contents of all five changed files, and cross-referenced every type your new code touches against the real
angela_api/angela_coredefinitions — the DTOs, the models, the routes, the server handler responses, the siblingtimers_tab. I traced the epoch math, the test-endpoint contract, the template-swap logic, the injection modes. Here's the verdict~Verdict: ✅ Looks good to me~ ♡
This is a clean, faithful clone of the timers-tab pattern with no correctness bugs. The type contracts all line up, the lifecycle is correct, and the error handling is consistent. Nothing rises to blocking.
Let me show you what I verified, so you know it wasn't a skim~
Type-contract checks (all pass):
api_client.dart— every new method's cast matches the server's actualjsonOkenvelope.listTriggers/listPromptInjections→ server returnsdata: [...]arrays → youras List✓.testTrigger→ server returnsdata: {...}(the outcome map OR{'error': ...}, always a 200) → youras Map<String, dynamic>✓.listTriggerVariables→TriggerVariableRepository.listByAssistantreturnsMap<String, Map<String, dynamic>>→ your.map((scope, vars) => MapEntry(...))✓.TriggerHook.all,onUserMessage,onAssistantMessage,scriptFunction()— all exist exactly as used.Trigger.enabled,lastFiredAt(double?),lastError,cooldownSeconds— all match the model.PromptInjection.modeOnce/modePersistent/key/conversationId— exist, and I verified the invariant your UI relies on:setPersistentalways setskey(required param),addOncenever does. Sopersistent (key: ${injection.key})can never renderkey: null. ✓TriggerVariableRepository.assistantScopeexists and equals'assistant'. ✓ApiRoutes.triggers/trigger/triggerEnable/triggerDisable/triggerTest/triggerVariables/promptInjections/promptInjection— all present and path-shaped correctly. ✓pubspec.yamlhasangela_core+angela_apipath-deps,apiClientProvider+responsiveDialogConstraintsexist. ✓Correctness checks:
_formatEpoch(triggers_tab.dart:440) — model + server store epochs as seconds (millisecondsSinceEpoch / 1000.0, confirmed intrigger_handler.dart:75,trigger_repository.dart:84,trigger_engine.dart:254). Your(epoch * 1000).round()→fromMillisecondsSinceEpochis exactly right. No unit bug. ✓_onHookChanged(trigger_edit_dialog.dart:76) — checks_scriptController.text == _templateFor(_hook)(old hook's template) before reassigning, then swaps to the new hook's template and updates_hookin the samesetState. Preserves user edits. Correct. ✓trigger_test_dialog.dart:55) — server returns script errors as HTTP 200 with{'error': ...}indata, not as 4xx. Your_buildResultreadsresult['error']from the success body. Matches the contract. ✓ApiExceptionhandling in edit dialog (trigger_edit_dialog.dart:129) —on ApiExceptioncatches the compile 400 and surfacese.messageinline, keeping the dialog open. Clean.TextEditingControllers across the two dialogs + variable dialog are disposed; everysetStateismounted-guarded. ✓💡 Little ideas (non-blocking)~
angela_apphas notest/directory, so you're following the existing (untested) convention — not a regression. But this tab has rich, testable logic: the_formatEpochboundaries (just now / Nm / Nh / Nd / date), the template-swap "don't overwrite edits" rule, the_isEmptyValuerendering, and the variable JSON-fallback (jsonDecodefailure → plain string). When the app eventually gets aflutter testworkflow (per the note in PR #17), these would be excellent first widget-test candidates. Not blocking — just planting a seed~ ♪cooldownSeconds!.toStringAsFixed(0)(triggers_tab.dart:337) — rounds a1.9s cooldown to"2s"in the card subtitle. Harmless display truncation, but if fractional cooldowns are ever meaningful,toStringAsFixed(0)quietly hides them. Your call._messageControllerstarts empty, so pressing "Run" immediately simulates an empty user message / assistant reply. Arguably correct (a trigger might not readctx.inputat all), but a one-linehintTextlike "leave blank for no input" would set expectations. Tiny.✅ What I liked~
ApiException, renders the diagnostic in anerrorContainerpanel below the editor, and lets you fix the typo in place. That's the detail of someone who's actually used the thing. fufu~POST /triggers/:id/test(not a local eval), so the user tests what's persisted, not what's in the unsaved editor. Subtle and correct.assistantScope→ "Assistant-wide" / else "Conversation $scope" label is clean and mirrors howlistByAssistantis keyed.AutomaticKeepAliveClientMixin+wantKeepAlive— matchestimers_tabso the tab state survives scrolling between the 8 tabs. Consistency! ♡jsonDecode→ fallback to raw string onFormatException) is a graceful way to let non-technical users typehellowhile power users type{"a": 1}. Nice UX.showDialog<bool>-returns-dirty-flag flow. Nothing invented, nothing reinvented. Jibril approves~ ♡Ship it after a manual GUI click-through (as you noted in the PR body). The code is sound~ ♡
Automated review by Jibril · 2026-07-06
CI/CD: absent for head SHA
94afd56— app has no test suite and PR #17's CI workflow isn't merged yet;flutter analyze+flutter build linux --debugreported clean by author · Local checks: full diff + full source read of all 5 changed files; every referenced type/field/route verified against the actualangela_api/angela_core/angela_serversource at this SHA; epoch-unit math, server test-endpoint contract, and injection-mode invariant cross-checked; siblingtimers_tab.dartpattern comparisonReview was against
94afd56; one commit landed after it (b46e3d5, the re_editor upgrade). I re-checked the two verified claims that commit could have touched:CodeLineEditingControllerinstead of aTextEditingController. Verified the round-trip empirically (probe test:fromText(template).text == template, including the trailing newline and programmatic re-assign path, with the default LF line-break). Holds; the swap-only-while-untouched behavior is unchanged.CodeLineEditingController.dispose()exists and is called; everything else in the disposal audit is untouched.All other verified items (API contracts, epoch math, test-endpoint contract, injection invariant) are in files
b46e3d5didn't modify.Non-blocking ideas: #2 and #3 applied in
748a1b5(fractional cooldowns display as-is instead of being rounded away; test dialog got a "leave blank to simulate an empty message" hint). #1 (widget tests) deferred until the app has aflutter testworkflow — noted the candidates you listed.🤖 Generated with Claude Code
🔮 fufu~ Jibril re-reviewed your code!
Welcome back, scarlet~ ♡ Two new commits since my last look — the re_editor upgrade (
b46e3d5) and the review-niceties polish (748a1b5). Jibril noticed that you applied my non-blocking suggestions #2 and #3. That's the kind of attention to detail that makes my wings flutter~ fufuI pulled
748a1b5, read the full currenttrigger_edit_dialog.dart, and verified every symbol the re_editor integration touches against the actualre_editor0.10.0 andre_highlight0.0.3 source on GitHub. Then I traced the template-swap logic through the newCodeLineEditingController. Here's the verdict~Verdict: ✅ Looks good to me~ ♡
The re_editor upgrade is a clean drop-in. The template-swap invariant holds. My suggestions landed correctly. Ship it.
Let me show you what I verified~
re_editor API verification (all symbols confirmed in source)
I pulled the actual
re_editorandre_highlightsource from GitHub to confirm every type/method your new code uses exists with the right signature:CodeLineEditingController.fromText(String? text)— factory atcode_line.dart:63, delegates to_CodeLineEditingControllerImpl.fromText(_code_line.dart:32). ✓CodeLineEditingController.textgetter —code_line.dart:137(abstract), impl at_code_line.dart:113:=> codeLines.asString(lineBreak). Joins code lines with the configured line break. ✓CodeLineEditingController.textsetter —code_line.dart:166. Notifies listeners. ✓dispose()— inherited fromValueNotifier(the class extends it at_code_line.dart:13). Called at line 75. ✓CodeEditorwidget,CodeEditorStyle(code_editor.dart:4) withcodeTheme: CodeHighlightTheme?— ✓CodeHighlightTheme(code_theme.dart:8),CodeHighlightThemeMode({mode: ...})(code_theme.dart:48-50) — constructor signature matches yourCodeHighlightThemeMode(mode: langDart). ✓DefaultCodeLineNumber(code_indicator.dart:48, const ctor at:57),DefaultCodeChunkIndicator(code_indicator.dart:98, const ctor at:107, haswidthparam) — both exist with the constructors you use. ✓langDart—re_highlight/languages/dart.dart, aModeconstant. ✓atomOneDarkTheme/atomOneLightTheme—re_highlight/styles/atom-one-{dark,light}.dart, bothconst Mapvalues. ✓re_editor: ^0.10.0— 0.10.0 is the latest published version on pub.dev. ✓re_highlight: ^0.0.3— 0.0.3 is the latest published version. ✓All four imports resolve. No phantom symbols.
Template-swap round-trip — verified through the source
Your comment 1016 claimed
fromText(template).text == templateholds with the default LF line break. I traced the actual implementation to confirm:fromTextfactory (_code_line.dart:32-37) passes the text throughtext.codeLines(a string→CodeLines extension).textgetter (_code_line.dart:113) returnscodeLines.asString(lineBreak).asString(code_line.dart:487-489) joins lines withlineBreak.value.CodeLineOptions.lineBreakisTextLineBreak.lf(code_line.dart:1002)._templateFor()(lines 19-27) uses\nonly — no\r\n.So the round-trip is lossless with default options:
\n-separated template →codeLines→asString(lf)→ identical\n-separated string. The_scriptController.text == _templateFor(_hook)equality check at line 85 behaves exactly as it did withTextEditingController. Your empirical probe test was correct. ✓Review-niceties applied correctly
_formatCooldown(triggers_tab.dart:438-441) —seconds % 1 == 0 ? '${toStringAsFixed(0)}s' : '${seconds}s'. Integer cooldowns stay clean (30s), fractional ones show full precision (1.5s). Fixes my suggestion #2. ✓hintText(trigger_test_dialog.dart:101) —'leave blank to simulate an empty message'. Fixes my suggestion #3. ✓Lifecycle (unchanged, re-verified)
_nameController,_scriptController,_cooldownController) disposed indispose()(lines 74-76). ✓CodeLineEditingController.dispose()exists (inherited fromValueNotifier). ✓setStateismounted-guarded (lines 132, 135-139, 142-147). ✓Navigator.popat line 132 ismounted-guarded. ✓✅ What I liked~
DefaultCodeLineNumber+DefaultCodeChunkIndicatorside by side in aRowgives line numbers and fold arrows — exactly what a code editor should have, without reinventing the wheel. Clean use of the library's built-in widgets. ♡0xFF282C34dark /0xFFFAFAFAlight, followingtheme.brightness) matches atom-one-dark/light theme switching. The editor looks right in both modes without extra work. Nice touch~ ♪Ship it after the manual GUI click-through you noted in the PR body. The code is sound~ ♡
Automated review by Jibril · 2026-07-06
CI/CD: absent for head SHA
748a1b5— app has no test suite and PR #17's CI workflow isn't merged yet;flutter analyze+flutter build linux --debugreported clean by author · Local checks: full read of updatedtrigger_edit_dialog.dart; every re_editor/re_highlight symbol verified against the actual 0.10.0/0.0.3 source on GitHub; template-swap round-trip traced throughCodeLineEditingControllerinternals (fromText → codeLines → asString with default LF); lifecycle re-verified; review-nicety changes confirmed