reviewflow 3.28.0 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/dashboard/index.html +74 -9
  3. package/dist/dashboard/modules/emberAvatar.d.ts +134 -0
  4. package/dist/dashboard/modules/emberAvatar.d.ts.map +1 -0
  5. package/dist/dashboard/modules/emberAvatar.js +192 -0
  6. package/dist/dashboard/modules/emberAvatar.js.map +1 -0
  7. package/dist/dashboard/modules/emberAvatarRenderer.d.ts +42 -0
  8. package/dist/dashboard/modules/emberAvatarRenderer.d.ts.map +1 -0
  9. package/dist/dashboard/modules/emberAvatarRenderer.js +119 -0
  10. package/dist/dashboard/modules/emberAvatarRenderer.js.map +1 -0
  11. package/dist/dashboard/modules/emberChat.d.ts +77 -0
  12. package/dist/dashboard/modules/emberChat.d.ts.map +1 -0
  13. package/dist/dashboard/modules/emberChat.js +175 -0
  14. package/dist/dashboard/modules/emberChat.js.map +1 -0
  15. package/dist/dashboard/styles.css +126 -0
  16. package/dist/main/routes.d.ts.map +1 -1
  17. package/dist/main/routes.js +32 -0
  18. package/dist/main/routes.js.map +1 -1
  19. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.d.ts +4 -0
  20. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.d.ts.map +1 -0
  21. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.js +4 -0
  22. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.js.map +1 -0
  23. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.d.ts +6 -0
  24. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.d.ts.map +1 -0
  25. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.js +5 -0
  26. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.js.map +1 -0
  27. package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.d.ts +7 -0
  28. package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.d.ts.map +1 -0
  29. package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.js +3 -0
  30. package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.js.map +1 -0
  31. package/dist/modules/ember-chat/entities/emberSession/emberSessionState.d.ts +13 -0
  32. package/dist/modules/ember-chat/entities/emberSession/emberSessionState.d.ts.map +1 -0
  33. package/dist/modules/ember-chat/entities/emberSession/emberSessionState.js +35 -0
  34. package/dist/modules/ember-chat/entities/emberSession/emberSessionState.js.map +1 -0
  35. package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.d.ts +26 -0
  36. package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.d.ts.map +1 -0
  37. package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.js +2 -0
  38. package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.js.map +1 -0
  39. package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.d.ts +11 -0
  40. package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.d.ts.map +1 -0
  41. package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.js +2 -0
  42. package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.js.map +1 -0
  43. package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.d.ts +15 -0
  44. package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.d.ts.map +1 -0
  45. package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.js +61 -0
  46. package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.js.map +1 -0
  47. package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.d.ts +24 -0
  48. package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.d.ts.map +1 -0
  49. package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.js +19 -0
  50. package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.js.map +1 -0
  51. package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.d.ts +13 -0
  52. package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.d.ts.map +1 -0
  53. package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.js +130 -0
  54. package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.js.map +1 -0
  55. package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.d.ts +18 -0
  56. package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.d.ts.map +1 -0
  57. package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.js +33 -0
  58. package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.js.map +1 -0
  59. package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.d.ts +20 -0
  60. package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.d.ts.map +1 -0
  61. package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.js +28 -0
  62. package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.js.map +1 -0
  63. package/dist/modules/ember-chat/services/emberSystemPrompt.d.ts +12 -0
  64. package/dist/modules/ember-chat/services/emberSystemPrompt.d.ts.map +1 -0
  65. package/dist/modules/ember-chat/services/emberSystemPrompt.js +29 -0
  66. package/dist/modules/ember-chat/services/emberSystemPrompt.js.map +1 -0
  67. package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.d.ts +23 -0
  68. package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.d.ts.map +1 -0
  69. package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.js +23 -0
  70. package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.js.map +1 -0
  71. package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.d.ts +36 -0
  72. package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.d.ts.map +1 -0
  73. package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.js +59 -0
  74. package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.js.map +1 -0
  75. package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.d.ts +2 -0
  76. package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.d.ts.map +1 -0
  77. package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.js +124 -0
  78. package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.js.map +1 -0
  79. package/dist/tests/factories/emberMessage.factory.d.ts +5 -0
  80. package/dist/tests/factories/emberMessage.factory.d.ts.map +1 -0
  81. package/dist/tests/factories/emberMessage.factory.js +9 -0
  82. package/dist/tests/factories/emberMessage.factory.js.map +1 -0
  83. package/dist/tests/stubs/emberReadData.stub.d.ts +20 -0
  84. package/dist/tests/stubs/emberReadData.stub.d.ts.map +1 -0
  85. package/dist/tests/stubs/emberReadData.stub.js +31 -0
  86. package/dist/tests/stubs/emberReadData.stub.js.map +1 -0
  87. package/dist/tests/stubs/emberSessionTransport.stub.d.ts +18 -0
  88. package/dist/tests/stubs/emberSessionTransport.stub.d.ts.map +1 -0
  89. package/dist/tests/stubs/emberSessionTransport.stub.js +75 -0
  90. package/dist/tests/stubs/emberSessionTransport.stub.js.map +1 -0
  91. package/dist/tests/units/dashboard/modules/emberAvatar.test.d.ts +2 -0
  92. package/dist/tests/units/dashboard/modules/emberAvatar.test.d.ts.map +1 -0
  93. package/dist/tests/units/dashboard/modules/emberAvatar.test.js +74 -0
  94. package/dist/tests/units/dashboard/modules/emberAvatar.test.js.map +1 -0
  95. package/dist/tests/units/dashboard/modules/emberAvatarRenderer.test.d.ts +2 -0
  96. package/dist/tests/units/dashboard/modules/emberAvatarRenderer.test.d.ts.map +1 -0
  97. package/dist/tests/units/dashboard/modules/emberAvatarRenderer.test.js +44 -0
  98. package/dist/tests/units/dashboard/modules/emberAvatarRenderer.test.js.map +1 -0
  99. package/dist/tests/units/dashboard/modules/emberChat.test.d.ts +2 -0
  100. package/dist/tests/units/dashboard/modules/emberChat.test.d.ts.map +1 -0
  101. package/dist/tests/units/dashboard/modules/emberChat.test.js +59 -0
  102. package/dist/tests/units/dashboard/modules/emberChat.test.js.map +1 -0
  103. package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.d.ts +2 -0
  104. package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.d.ts.map +1 -0
  105. package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.js +101 -0
  106. package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.js.map +1 -0
  107. package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.d.ts +2 -0
  108. package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.d.ts.map +1 -0
  109. package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.js +24 -0
  110. package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.js.map +1 -0
  111. package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.d.ts +2 -0
  112. package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.d.ts.map +1 -0
  113. package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.js +42 -0
  114. package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.js.map +1 -0
  115. package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.d.ts +2 -0
  116. package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.d.ts.map +1 -0
  117. package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.js +75 -0
  118. package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.js.map +1 -0
  119. package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.d.ts +2 -0
  120. package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.d.ts.map +1 -0
  121. package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.js +52 -0
  122. package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.js.map +1 -0
  123. package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.d.ts +2 -0
  124. package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.d.ts.map +1 -0
  125. package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.js +27 -0
  126. package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.js.map +1 -0
  127. package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.d.ts +2 -0
  128. package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.d.ts.map +1 -0
  129. package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.js +41 -0
  130. package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.js.map +1 -0
  131. package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.d.ts +2 -0
  132. package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.d.ts.map +1 -0
  133. package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.js +46 -0
  134. package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.js.map +1 -0
  135. package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.d.ts +2 -0
  136. package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.d.ts.map +1 -0
  137. package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.js +88 -0
  138. package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.js.map +1 -0
  139. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.30.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.29.0...reviewflow-v3.30.0) (2026-05-28)
9
+
10
+
11
+ ### Added
12
+
13
+ * **dashboard:** SPEC-189 Ember flame wireframe avatar + sidebar layout ([#248](https://github.com/DGouron/review-flow/issues/248)) ([7e82a2f](https://github.com/DGouron/review-flow/commit/7e82a2fb385c8990558e3fd15e103b2e8bbd40e7))
14
+
15
+ ## [3.29.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.28.0...reviewflow-v3.29.0) (2026-05-28)
16
+
17
+
18
+ ### Added
19
+
20
+ * **dashboard:** SPEC-189 Ember read-only review chat (Phase A) ([#244](https://github.com/DGouron/review-flow/issues/244)) ([4e0d5b9](https://github.com/DGouron/review-flow/commit/4e0d5b9bb37a68cc2c91454412a35ce34107f1ed))
21
+
22
+
23
+ ### Fixed
24
+
25
+ * **docs:** unblock vitepress build and refresh setup-wizard docs ([83ce3bb](https://github.com/DGouron/review-flow/commit/83ce3bbbea6e80851742c8af08cfbb912bdc66d5))
26
+ * **docs:** unblock vitepress build and refresh setup-wizard docs ([fc5896d](https://github.com/DGouron/review-flow/commit/fc5896d14cc378d51bf17c9f51ebc01360cbc6bb))
27
+
8
28
  ## [3.28.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.27.0...reviewflow-v3.28.0) (2026-05-28)
9
29
 
10
30
 
@@ -16,6 +36,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
36
  * **setup-wizard:** SPEC-184 Iteration B — dashboard wizard interactive forms ([#238](https://github.com/DGouron/review-flow/issues/238)) ([806515b](https://github.com/DGouron/review-flow/commit/806515b4a69efbfaf8022185e545393dc7525623))
17
37
  * **setup-wizard:** SPEC-187 read wizard answers from stdin in JSON mode ([#237](https://github.com/DGouron/review-flow/issues/237)) ([8476550](https://github.com/DGouron/review-flow/commit/8476550edbfce72743e882e2c4a907d60c5ab9a4))
18
38
 
39
+
40
+ ### Fixed
41
+
42
+ * **worktree:** drop false-positive missing-build-artifacts signal ([#239](https://github.com/DGouron/review-flow/issues/239)) ([41a3a79](https://github.com/DGouron/review-flow/commit/41a3a79b5bec924f895ebfa35197d003fde95dd3))
43
+
19
44
  ## [3.27.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.26.0...reviewflow-v3.27.0) (2026-05-27)
20
45
 
21
46
 
@@ -101,18 +101,39 @@
101
101
 
102
102
  <div class="dashboard-layout">
103
103
  <aside class="dashboard-sidebar" aria-label="Project tools">
104
- <button type="button" id="open-settings-modal-btn" class="sidebar-settings-button" hidden>
105
- <span class="sidebar-settings-button__prefix">// SETTINGS</span>
106
- </button>
107
- <button type="button" id="open-economics-sheet-btn" class="sidebar-settings-button" onclick="openEconomicsSheet()">
108
- <span class="sidebar-settings-button__prefix">// CLAUDE ECONOMICS</span>
109
- </button>
110
- <button type="button" id="open-stats-sheet-btn" class="sidebar-settings-button" onclick="openStatsSheet()" disabled aria-disabled="true">
111
- <span class="sidebar-settings-button__prefix">// PROJECT STATS</span>
112
- </button>
104
+ <div class="sidebar-tool-buttons">
105
+ <button type="button" id="open-settings-modal-btn" class="sidebar-settings-button" hidden>
106
+ <span class="sidebar-settings-button__prefix">// SETTINGS</span>
107
+ </button>
108
+ <button type="button" id="open-economics-sheet-btn" class="sidebar-settings-button" onclick="openEconomicsSheet()">
109
+ <span class="sidebar-settings-button__prefix">// ECONOMICS</span>
110
+ </button>
111
+ <button type="button" id="open-stats-sheet-btn" class="sidebar-settings-button" onclick="openStatsSheet()" disabled aria-disabled="true">
112
+ <span class="sidebar-settings-button__prefix">// STATS</span>
113
+ </button>
114
+ </div>
113
115
 
114
116
  <span id="config-status" class="config-status hidden"></span>
115
117
 
118
+ <section id="ember-chat-panel" class="ember-chat-panel" aria-label="Ember">
119
+ <div class="ember-chat-panel__header">
120
+ <span class="ember-chat-panel__prefix">// EMBER</span>
121
+ <canvas id="ember-avatar" class="ember-chat-panel__avatar" width="360" height="360" aria-hidden="true"></canvas>
122
+ </div>
123
+ <output id="ember-answer" class="ember-chat-panel__answer" aria-live="polite"></output>
124
+ <div id="ember-status" class="ember-chat-panel__status" role="status" aria-live="polite"></div>
125
+ <button type="button" id="ember-retry" class="ember-chat-panel__retry" hidden>// RÉESSAYER</button>
126
+ <form id="ember-form" class="ember-chat-panel__form" autocomplete="off">
127
+ <label class="visually-hidden" for="ember-question">Posez une question à Ember</label>
128
+ <input
129
+ type="text"
130
+ id="ember-question"
131
+ class="ember-chat-panel__input"
132
+ placeholder="Posez une question sur vos reviews…"
133
+ />
134
+ </form>
135
+ </section>
136
+
116
137
  <section id="worktree-section" aria-label="Worktree pool"></section>
117
138
  </aside>
118
139
 
@@ -429,6 +450,8 @@
429
450
  buildHeaderCapacityViewModel,
430
451
  renderHeaderCapacityBadgeHtml,
431
452
  } from './modules/headerCapacityBadge.js';
453
+ import { connectEmberStream, shouldSendQuestion } from './modules/emberChat.js';
454
+ import { mountEmberAvatar } from './modules/emberAvatarRenderer.js';
432
455
 
433
456
  const API_URL = window.location.origin;
434
457
  const WS_URL = `ws://${window.location.host}/ws`;
@@ -3838,6 +3861,47 @@
3838
3861
  }).catch(() => {});
3839
3862
  });
3840
3863
 
3864
+ // Ember read-only chat (SPEC-189). The pure decisions + SSE fold live in
3865
+ // emberChat.js; this humble init just binds DOM and the reused avatar.
3866
+ function initEmberChat() {
3867
+ const form = document.getElementById('ember-form');
3868
+ const input = document.getElementById('ember-question');
3869
+ const answer = document.getElementById('ember-answer');
3870
+ const status = document.getElementById('ember-status');
3871
+ const retry = document.getElementById('ember-retry');
3872
+ const canvas = document.getElementById('ember-avatar');
3873
+ const panel = document.getElementById('ember-chat-panel');
3874
+ if (!form || !input || !answer || !status || !canvas) return;
3875
+
3876
+ const avatar = mountEmberAvatar({ canvas, initialState: 'idle' });
3877
+
3878
+ const ask = async () => {
3879
+ const question = input.value;
3880
+ if (!shouldSendQuestion(question)) {
3881
+ input.focus();
3882
+ return;
3883
+ }
3884
+ panel?.classList.add('ember-chat-panel--active');
3885
+ answer.textContent = '';
3886
+ status.textContent = '';
3887
+ retry.hidden = true;
3888
+ await connectEmberStream({
3889
+ question,
3890
+ onAnswer: (text) => { answer.textContent = text; },
3891
+ onAvatarState: (state) => avatar.setState(state),
3892
+ onAnnounce: (text) => { status.textContent = text; },
3893
+ onRetryVisible: (visible) => { retry.hidden = !visible; },
3894
+ });
3895
+ input.focus();
3896
+ };
3897
+
3898
+ form.addEventListener('submit', (event) => {
3899
+ event.preventDefault();
3900
+ void ask();
3901
+ });
3902
+ retry.addEventListener('click', () => { void ask(); });
3903
+ }
3904
+
3841
3905
  // Boot animations & observers exactly once, regardless of readyState timing
3842
3906
  let __animationsBooted = false;
3843
3907
  function bootOnce() {
@@ -3845,6 +3909,7 @@
3845
3909
  __animationsBooted = true;
3846
3910
  bootAnimations();
3847
3911
  observeCounters();
3912
+ initEmberChat();
3848
3913
  }
3849
3914
  if (document.readyState !== 'loading') {
3850
3915
  bootOnce();
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Maps an ember state to its stroke/animation parameters. Keeps the renderer
3
+ * humble: every per-state decision lives here, not in the canvas loop.
4
+ *
5
+ * @param {EmberState} state
6
+ * @returns {EmberVisual}
7
+ */
8
+ export function emberStateToVisual(state: EmberState): EmberVisual;
9
+ /**
10
+ * Builds the flame wireframe as a surface of revolution: a single tip vertex
11
+ * (index 0) plus `rings` × `meridians` body vertices, joined by tip spokes,
12
+ * vertical meridian lines and horizontal ring loops. Pure and deterministic.
13
+ *
14
+ * @param {{ rings: number; meridians: number }} options
15
+ * @returns {{ vertices: Vertex[]; edges: Array<[number, number]> }}
16
+ */
17
+ export function buildFlameWireframe(options: {
18
+ rings: number;
19
+ meridians: number;
20
+ }): {
21
+ vertices: Vertex[];
22
+ edges: Array<[number, number]>;
23
+ };
24
+ /**
25
+ * @typedef {Object} Projection
26
+ * @property {number} tilt Fixed X-axis tilt in radians.
27
+ * @property {number} distance Camera distance for the perspective divide.
28
+ * @property {number} scale Pixels per unit at the projection plane.
29
+ * @property {number} centerX Canvas-space x offset.
30
+ * @property {number} centerY Canvas-space y offset.
31
+ */
32
+ /**
33
+ * Projects a flame vertex to a 2D canvas point: rotates around Y, leans the
34
+ * upper body sideways by `swayOffset` scaled by height (a candle flame bends at
35
+ * the tip, not the base), applies the fixed tilt, then a perspective divide.
36
+ * Pure and deterministic.
37
+ *
38
+ * @param {Vertex} vertex
39
+ * @param {number} rotationRadians
40
+ * @param {number} swayOffset
41
+ * @param {Projection} projection
42
+ * @returns {{ x: number; y: number }}
43
+ */
44
+ export function projectFlameVertex(vertex: Vertex, rotationRadians: number, swayOffset: number, projection: Projection): {
45
+ x: number;
46
+ y: number;
47
+ };
48
+ export function emberRadiusFactor(visual: any, time: any): number;
49
+ /**
50
+ * The horizontal lean of the flame tip at a given time. Pure — 0 at time 0,
51
+ * always within ± swayAmount.
52
+ *
53
+ * @param {EmberVisual} visual
54
+ * @param {number} time Milliseconds since the loop started.
55
+ * @returns {number}
56
+ */
57
+ export function emberSwayOffset(visual: EmberVisual, time: number): number;
58
+ /**
59
+ * Dashboard module — Ember flame wireframe avatar (SPEC-189).
60
+ * Humble object: pure functions, no DOM, no global state. Holds the flame-shaped
61
+ * wireframe geometry and every per-state visual decision, so the
62
+ * requestAnimationFrame loop in emberAvatarRenderer.js carries no branching.
63
+ *
64
+ * Ember is a warm wireframe BRAISE, not the setup wizard's abstract icosahedron:
65
+ * a teardrop flame mesh (pointed tip, rounded base) stroked in amber, that leans
66
+ * and flickers like a live coal — livelier and warmer when spoken to.
67
+ *
68
+ * Visual DNA: "Agentic OS" — dark warm near-black + amber. See
69
+ * project_agentic_os_design_dna.md.
70
+ */
71
+ /**
72
+ * @typedef {'idle' | 'working' | 'error'} EmberState
73
+ */
74
+ /** @type {EmberState[]} */
75
+ export const EMBER_STATES: EmberState[];
76
+ /** Top of the flame (the tip) in model space. */
77
+ export const FLAME_TIP_Y: 1.5;
78
+ /** Bottom of the flame (the rounded base) in model space. */
79
+ export const FLAME_BASE_Y: -1.15;
80
+ export type Vertex = [number, number, number];
81
+ export type Projection = {
82
+ /**
83
+ * Fixed X-axis tilt in radians.
84
+ */
85
+ tilt: number;
86
+ /**
87
+ * Camera distance for the perspective divide.
88
+ */
89
+ distance: number;
90
+ /**
91
+ * Pixels per unit at the projection plane.
92
+ */
93
+ scale: number;
94
+ /**
95
+ * Canvas-space x offset.
96
+ */
97
+ centerX: number;
98
+ /**
99
+ * Canvas-space y offset.
100
+ */
101
+ centerY: number;
102
+ };
103
+ export type EmberState = "idle" | "working" | "error";
104
+ export type EmberVisual = {
105
+ /**
106
+ * CSS custom-property name driving the warm stroke.
107
+ */
108
+ color: string;
109
+ /**
110
+ * Stroke width in device pixels.
111
+ */
112
+ lineWidth: number;
113
+ /**
114
+ * Radians per second of the slow Y rotation.
115
+ */
116
+ rotationSpeed: number;
117
+ /**
118
+ * Radians per millisecond of the candle-lean sway.
119
+ */
120
+ swaySpeed: number;
121
+ /**
122
+ * Horizontal lean amplitude at the flame tip.
123
+ */
124
+ swayAmount: number;
125
+ /**
126
+ * Amplitude of the scale shimmer (the coal breathing).
127
+ */
128
+ flicker: number;
129
+ /**
130
+ * Stroke shadow-blur in pixels (the warm halo around lines).
131
+ */
132
+ glow: number;
133
+ };
134
+ //# sourceMappingURL=emberAvatar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberAvatar.d.ts","sourceRoot":"","sources":["../../../src/dashboard/modules/emberAvatar.js"],"names":[],"mappings":"AA4CA;;;;;;GAMG;AACH,0CAHW,UAAU,GACR,WAAW,CAIvB;AAmBD;;;;;;;GAOG;AACH,6CAHW;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAmClE;AAED;;;;;;;GAOG;AAEH;;;;;;;;;;;GAWG;AACH,2CANW,MAAM,mBACN,MAAM,cACN,MAAM,cACN,UAAU,GACR;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAuBpC;AAgBD,kEAMC;AAED;;;;;;;GAOG;AACH,wCAJW,WAAW,QACX,MAAM,GACJ,MAAM,CAIlB;AA/LD;;;;;;;;;;;;GAYG;AAEH;;GAEG;AAEH,2BAA2B;AAC3B,2BADW,UAAU,EAAE,CACkC;AAEzD,iDAAiD;AACjD,0BAA2B,GAAG,CAAC;AAC/B,6DAA6D;AAC7D,2BAA4B,CAAC,IAAI,CAAC;qBAgCrB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;UA6DvB,MAAM;;;;cACN,MAAM;;;;WACN,MAAM;;;;aACN,MAAM;;;;aACN,MAAM;;yBA1GP,MAAM,GAAG,SAAS,GAAG,OAAO;;;;;WAa3B,MAAM;;;;eACN,MAAM;;;;mBACN,MAAM;;;;eACN,MAAM;;;;gBACN,MAAM;;;;aACN,MAAM;;;;UACN,MAAM"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Dashboard module — Ember flame wireframe avatar (SPEC-189).
3
+ * Humble object: pure functions, no DOM, no global state. Holds the flame-shaped
4
+ * wireframe geometry and every per-state visual decision, so the
5
+ * requestAnimationFrame loop in emberAvatarRenderer.js carries no branching.
6
+ *
7
+ * Ember is a warm wireframe BRAISE, not the setup wizard's abstract icosahedron:
8
+ * a teardrop flame mesh (pointed tip, rounded base) stroked in amber, that leans
9
+ * and flickers like a live coal — livelier and warmer when spoken to.
10
+ *
11
+ * Visual DNA: "Agentic OS" — dark warm near-black + amber. See
12
+ * project_agentic_os_design_dna.md.
13
+ */
14
+
15
+ /**
16
+ * @typedef {'idle' | 'working' | 'error'} EmberState
17
+ */
18
+
19
+ /** @type {EmberState[]} */
20
+ export const EMBER_STATES = ['idle', 'working', 'error'];
21
+
22
+ /** Top of the flame (the tip) in model space. */
23
+ export const FLAME_TIP_Y = 1.5;
24
+ /** Bottom of the flame (the rounded base) in model space. */
25
+ export const FLAME_BASE_Y = -1.15;
26
+
27
+ /**
28
+ * @typedef {Object} EmberVisual
29
+ * @property {string} color CSS custom-property name driving the warm stroke.
30
+ * @property {number} lineWidth Stroke width in device pixels.
31
+ * @property {number} rotationSpeed Radians per second of the slow Y rotation.
32
+ * @property {number} swaySpeed Radians per millisecond of the candle-lean sway.
33
+ * @property {number} swayAmount Horizontal lean amplitude at the flame tip.
34
+ * @property {number} flicker Amplitude of the scale shimmer (the coal breathing).
35
+ * @property {number} glow Stroke shadow-blur in pixels (the warm halo around lines).
36
+ */
37
+
38
+ /** @type {Record<EmberState, EmberVisual>} */
39
+ const EMBER_VISUALS = {
40
+ idle: { color: '--accent', lineWidth: 1.5, rotationSpeed: 0.3, swaySpeed: 0.0011, swayAmount: 0.08, flicker: 0.025, glow: 10 },
41
+ working: { color: '--accent', lineWidth: 1.8, rotationSpeed: 0.6, swaySpeed: 0.0026, swayAmount: 0.22, flicker: 0.16, glow: 16 },
42
+ error: { color: '--danger', lineWidth: 2, rotationSpeed: 0.18, swaySpeed: 0.0018, swayAmount: 0.14, flicker: 0.22, glow: 12 },
43
+ };
44
+
45
+ /**
46
+ * Maps an ember state to its stroke/animation parameters. Keeps the renderer
47
+ * humble: every per-state decision lives here, not in the canvas loop.
48
+ *
49
+ * @param {EmberState} state
50
+ * @returns {EmberVisual}
51
+ */
52
+ export function emberStateToVisual(state) {
53
+ return EMBER_VISUALS[state];
54
+ }
55
+
56
+ /**
57
+ * @typedef {[number, number, number]} Vertex
58
+ */
59
+
60
+ /**
61
+ * The flame radius at a normalised height ringT ∈ [0,1] (0 near the tip, 1 at the
62
+ * base). A teardrop profile: pointed at the top, bulging in the lower third, a
63
+ * small rounded base — the silhouette that reads as a coal/flame rather than a
64
+ * ball.
65
+ *
66
+ * @param {number} ringT
67
+ * @returns {number}
68
+ */
69
+ function flameRadius(ringT) {
70
+ return Math.sin(Math.PI * ringT ** 1.3) * 0.9 + 0.06;
71
+ }
72
+
73
+ /**
74
+ * Builds the flame wireframe as a surface of revolution: a single tip vertex
75
+ * (index 0) plus `rings` × `meridians` body vertices, joined by tip spokes,
76
+ * vertical meridian lines and horizontal ring loops. Pure and deterministic.
77
+ *
78
+ * @param {{ rings: number; meridians: number }} options
79
+ * @returns {{ vertices: Vertex[]; edges: Array<[number, number]> }}
80
+ */
81
+ export function buildFlameWireframe(options) {
82
+ const { rings, meridians } = options;
83
+ /** @type {Vertex[]} */
84
+ const vertices = [[0, FLAME_TIP_Y, 0]];
85
+ const indexAt = (ring, meridian) => 1 + ring * meridians + meridian;
86
+
87
+ for (let ring = 0; ring < rings; ring += 1) {
88
+ const ringT = (ring + 1) / rings;
89
+ const y = FLAME_TIP_Y + (FLAME_BASE_Y - FLAME_TIP_Y) * ringT;
90
+ const radius = flameRadius(ringT);
91
+ for (let meridian = 0; meridian < meridians; meridian += 1) {
92
+ const angle = (meridian / meridians) * Math.PI * 2;
93
+ vertices.push([Math.cos(angle) * radius, y, Math.sin(angle) * radius]);
94
+ }
95
+ }
96
+
97
+ /** @type {Array<[number, number]>} */
98
+ const edges = [];
99
+ for (let meridian = 0; meridian < meridians; meridian += 1) {
100
+ edges.push([0, indexAt(0, meridian)]);
101
+ }
102
+ for (let ring = 0; ring < rings - 1; ring += 1) {
103
+ for (let meridian = 0; meridian < meridians; meridian += 1) {
104
+ edges.push([indexAt(ring, meridian), indexAt(ring + 1, meridian)]);
105
+ }
106
+ }
107
+ for (let ring = 0; ring < rings; ring += 1) {
108
+ for (let meridian = 0; meridian < meridians; meridian += 1) {
109
+ edges.push([indexAt(ring, meridian), indexAt(ring, (meridian + 1) % meridians)]);
110
+ }
111
+ }
112
+
113
+ return { vertices, edges };
114
+ }
115
+
116
+ /**
117
+ * @typedef {Object} Projection
118
+ * @property {number} tilt Fixed X-axis tilt in radians.
119
+ * @property {number} distance Camera distance for the perspective divide.
120
+ * @property {number} scale Pixels per unit at the projection plane.
121
+ * @property {number} centerX Canvas-space x offset.
122
+ * @property {number} centerY Canvas-space y offset.
123
+ */
124
+
125
+ /**
126
+ * Projects a flame vertex to a 2D canvas point: rotates around Y, leans the
127
+ * upper body sideways by `swayOffset` scaled by height (a candle flame bends at
128
+ * the tip, not the base), applies the fixed tilt, then a perspective divide.
129
+ * Pure and deterministic.
130
+ *
131
+ * @param {Vertex} vertex
132
+ * @param {number} rotationRadians
133
+ * @param {number} swayOffset
134
+ * @param {Projection} projection
135
+ * @returns {{ x: number; y: number }}
136
+ */
137
+ export function projectFlameVertex(vertex, rotationRadians, swayOffset, projection) {
138
+ const [x, y, z] = vertex;
139
+
140
+ const cosY = Math.cos(rotationRadians);
141
+ const sinY = Math.sin(rotationRadians);
142
+ const rotatedX = x * cosY + z * sinY;
143
+ const rotatedZ = -x * sinY + z * cosY;
144
+
145
+ const heightFactor = (y - FLAME_BASE_Y) / (FLAME_TIP_Y - FLAME_BASE_Y);
146
+ const leanedX = rotatedX + swayOffset * heightFactor;
147
+
148
+ const cosTilt = Math.cos(projection.tilt);
149
+ const sinTilt = Math.sin(projection.tilt);
150
+ const tiltedY = y * cosTilt - rotatedZ * sinTilt;
151
+ const tiltedZ = y * sinTilt + rotatedZ * cosTilt;
152
+
153
+ const perspective = projection.scale / (tiltedZ + projection.distance);
154
+ return {
155
+ x: projection.centerX + leanedX * perspective,
156
+ y: projection.centerY - tiltedY * perspective,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * The breathing multiplier applied to the flame scale at a given time: a slow
162
+ * pulse plus a faster two-tone shimmer that reads as flicker. Pure — 1 at time 0,
163
+ * always within 1 ± flicker.
164
+ *
165
+ * @param {EmberVisual} visual
166
+ * @param {number} time Milliseconds since the loop started.
167
+ * @returns {number}
168
+ */
169
+ const SHIMMER_SLOW_FREQUENCY = 0.013;
170
+ const SHIMMER_FAST_FREQUENCY = 0.031;
171
+ const SHIMMER_SLOW_WEIGHT = 0.6;
172
+ const SHIMMER_FAST_WEIGHT = 0.4;
173
+
174
+ export function emberRadiusFactor(visual, time) {
175
+ const shimmer =
176
+ visual.flicker *
177
+ (SHIMMER_SLOW_WEIGHT * Math.sin(time * SHIMMER_SLOW_FREQUENCY) +
178
+ SHIMMER_FAST_WEIGHT * Math.sin(time * SHIMMER_FAST_FREQUENCY));
179
+ return 1 + shimmer;
180
+ }
181
+
182
+ /**
183
+ * The horizontal lean of the flame tip at a given time. Pure — 0 at time 0,
184
+ * always within ± swayAmount.
185
+ *
186
+ * @param {EmberVisual} visual
187
+ * @param {number} time Milliseconds since the loop started.
188
+ * @returns {number}
189
+ */
190
+ export function emberSwayOffset(visual, time) {
191
+ return visual.swayAmount * Math.sin(time * visual.swaySpeed);
192
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberAvatar.js","sourceRoot":"","sources":["../../../src/dashboard/modules/emberAvatar.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The structural slice of a canvas the renderer depends on. A real
3
+ * HTMLCanvasElement satisfies it; tests pass a lightweight fake without casts.
4
+ *
5
+ * @typedef {Object} EmberCanvas
6
+ * @property {number} width
7
+ * @property {number} height
8
+ * @property {(contextId: '2d') => CanvasRenderingContext2D | null} getContext
9
+ */
10
+ /**
11
+ * Mounts the animated flame wireframe avatar on a 2D canvas and returns its
12
+ * controls. The page calls setState() on each stream event and destroy() before
13
+ * any teardown so the rAF loop never leaks.
14
+ *
15
+ * @param {Object} options
16
+ * @param {EmberCanvas} options.canvas
17
+ * @param {import('./emberAvatar.js').EmberState} [options.initialState]
18
+ * @param {(callback: FrameRequestCallback) => number} [options.requestFrame]
19
+ * @param {(handle: number) => void} [options.cancelFrame]
20
+ * @param {() => number} [options.now]
21
+ * @returns {{ setState: (state: import('./emberAvatar.js').EmberState) => void; destroy: () => void }}
22
+ */
23
+ export function mountEmberAvatar(options: {
24
+ canvas: EmberCanvas;
25
+ initialState?: import("./emberAvatar.js").EmberState | undefined;
26
+ requestFrame?: ((callback: FrameRequestCallback) => number) | undefined;
27
+ cancelFrame?: ((handle: number) => void) | undefined;
28
+ now?: (() => number) | undefined;
29
+ }): {
30
+ setState: (state: import("./emberAvatar.js").EmberState) => void;
31
+ destroy: () => void;
32
+ };
33
+ /**
34
+ * The structural slice of a canvas the renderer depends on. A real
35
+ * HTMLCanvasElement satisfies it; tests pass a lightweight fake without casts.
36
+ */
37
+ export type EmberCanvas = {
38
+ width: number;
39
+ height: number;
40
+ getContext: (contextId: "2d") => CanvasRenderingContext2D | null;
41
+ };
42
+ //# sourceMappingURL=emberAvatarRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberAvatarRenderer.d.ts","sourceRoot":"","sources":["../../../src/dashboard/modules/emberAvatarRenderer.js"],"names":[],"mappings":"AA8BA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,0CAPG;IAA6B,MAAM,EAA3B,WAAW;IACqC,YAAY;IACP,YAAY,eAAtD,oBAAoB,KAAK,MAAM;IACP,WAAW,aAArC,MAAM,KAAK,IAAI;IACD,GAAG,UAApB,MAAM;CACpB,GAAU;IAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,kBAAkB,EAAE,UAAU,KAAK,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAmErG;;;;;;WAnFa,MAAM;YACN,MAAM;gBACN,CAAC,SAAS,EAAE,IAAI,KAAK,wBAAwB,GAAG,IAAI"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Dashboard module — Ember flame wireframe renderer (SPEC-189).
3
+ * Humble glue: a thin requestAnimationFrame loop that strokes the flame
4
+ * wireframe with a warm amber glow (shadowBlur), a slow Y rotation, a candle
5
+ * lean and a flicker breathing. It owns the rAF handle and its teardown, and
6
+ * delegates EVERY decision (geometry, colour, line width, rotation, sway,
7
+ * flicker, glow) to the pure emberAvatar.js module. Lifecycle-tested only
8
+ * (browser-only drawing), mirroring setupWizardAvatarRenderer.
9
+ *
10
+ * Visual DNA: "Agentic OS" — dark warm near-black + amber.
11
+ */
12
+
13
+ import {
14
+ emberStateToVisual,
15
+ buildFlameWireframe,
16
+ projectFlameVertex,
17
+ emberRadiusFactor,
18
+ emberSwayOffset,
19
+ } from './emberAvatar.js';
20
+
21
+ /**
22
+ * @param {CanvasRenderingContext2D} context
23
+ * @param {string} colorToken
24
+ * @returns {string}
25
+ */
26
+ function resolveColor(context, colorToken) {
27
+ const value = getComputedStyle(context.canvas).getPropertyValue(colorToken).trim();
28
+ return value === '' ? '#F4A93D' : value;
29
+ }
30
+
31
+ /**
32
+ * The structural slice of a canvas the renderer depends on. A real
33
+ * HTMLCanvasElement satisfies it; tests pass a lightweight fake without casts.
34
+ *
35
+ * @typedef {Object} EmberCanvas
36
+ * @property {number} width
37
+ * @property {number} height
38
+ * @property {(contextId: '2d') => CanvasRenderingContext2D | null} getContext
39
+ */
40
+
41
+ /**
42
+ * Mounts the animated flame wireframe avatar on a 2D canvas and returns its
43
+ * controls. The page calls setState() on each stream event and destroy() before
44
+ * any teardown so the rAF loop never leaks.
45
+ *
46
+ * @param {Object} options
47
+ * @param {EmberCanvas} options.canvas
48
+ * @param {import('./emberAvatar.js').EmberState} [options.initialState]
49
+ * @param {(callback: FrameRequestCallback) => number} [options.requestFrame]
50
+ * @param {(handle: number) => void} [options.cancelFrame]
51
+ * @param {() => number} [options.now]
52
+ * @returns {{ setState: (state: import('./emberAvatar.js').EmberState) => void; destroy: () => void }}
53
+ */
54
+ export function mountEmberAvatar(options) {
55
+ const requestFrame = options.requestFrame ?? globalThis.requestAnimationFrame.bind(globalThis);
56
+ const cancelFrame = options.cancelFrame ?? globalThis.cancelAnimationFrame.bind(globalThis);
57
+ const now = options.now ?? (() => performance.now());
58
+ const context = options.canvas.getContext('2d');
59
+ const startTime = now();
60
+ const flame = buildFlameWireframe({ rings: 7, meridians: 10 });
61
+
62
+ let currentState = options.initialState ?? 'idle';
63
+ /** @type {number | null} */
64
+ let frameHandle = null;
65
+
66
+ const projection = {
67
+ tilt: 0.12,
68
+ distance: 4,
69
+ scale: Math.min(options.canvas.width, options.canvas.height) * 0.55,
70
+ centerX: options.canvas.width / 2,
71
+ centerY: options.canvas.height / 2,
72
+ };
73
+
74
+ /**
75
+ * @param {number} time
76
+ */
77
+ function draw(time) {
78
+ if (context === null) {
79
+ return;
80
+ }
81
+ const elapsed = time - startTime;
82
+ const visual = emberStateToVisual(currentState);
83
+ const rotation = (elapsed * visual.rotationSpeed) / 1000;
84
+ const sway = emberSwayOffset(visual, elapsed);
85
+ const framedProjection = { ...projection, scale: projection.scale * emberRadiusFactor(visual, elapsed) };
86
+
87
+ context.clearRect(0, 0, options.canvas.width, options.canvas.height);
88
+ const color = resolveColor(context, visual.color);
89
+ context.lineWidth = visual.lineWidth;
90
+ context.lineCap = 'round';
91
+ context.strokeStyle = color;
92
+ context.shadowColor = color;
93
+ context.shadowBlur = visual.glow;
94
+ context.beginPath();
95
+ for (const [from, to] of flame.edges) {
96
+ const start = projectFlameVertex(flame.vertices[from], rotation, sway, framedProjection);
97
+ const end = projectFlameVertex(flame.vertices[to], rotation, sway, framedProjection);
98
+ context.moveTo(start.x, start.y);
99
+ context.lineTo(end.x, end.y);
100
+ }
101
+ context.stroke();
102
+
103
+ frameHandle = requestFrame(draw);
104
+ }
105
+
106
+ frameHandle = requestFrame(draw);
107
+
108
+ return {
109
+ setState: (state) => {
110
+ currentState = state;
111
+ },
112
+ destroy: () => {
113
+ if (frameHandle !== null) {
114
+ cancelFrame(frameHandle);
115
+ frameHandle = null;
116
+ }
117
+ },
118
+ };
119
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberAvatarRenderer.js","sourceRoot":"","sources":["../../../src/dashboard/modules/emberAvatarRenderer.js"],"names":[],"mappings":"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"}