reviewflow 3.29.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ 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
+
8
15
  ## [3.29.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.28.0...reviewflow-v3.29.0) (2026-05-28)
9
16
 
10
17
 
@@ -101,24 +101,24 @@
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
 
116
- <section id="worktree-section" aria-label="Worktree pool"></section>
117
-
118
118
  <section id="ember-chat-panel" class="ember-chat-panel" aria-label="Ember">
119
119
  <div class="ember-chat-panel__header">
120
120
  <span class="ember-chat-panel__prefix">// EMBER</span>
121
- <canvas id="ember-avatar" class="ember-chat-panel__avatar" width="72" height="72" aria-hidden="true"></canvas>
121
+ <canvas id="ember-avatar" class="ember-chat-panel__avatar" width="360" height="360" aria-hidden="true"></canvas>
122
122
  </div>
123
123
  <output id="ember-answer" class="ember-chat-panel__answer" aria-live="polite"></output>
124
124
  <div id="ember-status" class="ember-chat-panel__status" role="status" aria-live="polite"></div>
@@ -133,6 +133,8 @@
133
133
  />
134
134
  </form>
135
135
  </section>
136
+
137
+ <section id="worktree-section" aria-label="Worktree pool"></section>
136
138
  </aside>
137
139
 
138
140
  <main class="dashboard-main">
@@ -449,7 +451,7 @@
449
451
  renderHeaderCapacityBadgeHtml,
450
452
  } from './modules/headerCapacityBadge.js';
451
453
  import { connectEmberStream, shouldSendQuestion } from './modules/emberChat.js';
452
- import { mountSetupWizardAvatar } from './modules/setupWizardAvatarRenderer.js';
454
+ import { mountEmberAvatar } from './modules/emberAvatarRenderer.js';
453
455
 
454
456
  const API_URL = window.location.origin;
455
457
  const WS_URL = `ws://${window.location.host}/ws`;
@@ -3868,9 +3870,10 @@
3868
3870
  const status = document.getElementById('ember-status');
3869
3871
  const retry = document.getElementById('ember-retry');
3870
3872
  const canvas = document.getElementById('ember-avatar');
3873
+ const panel = document.getElementById('ember-chat-panel');
3871
3874
  if (!form || !input || !answer || !status || !canvas) return;
3872
3875
 
3873
- const avatar = mountSetupWizardAvatar({ canvas, initialState: 'idle' });
3876
+ const avatar = mountEmberAvatar({ canvas, initialState: 'idle' });
3874
3877
 
3875
3878
  const ask = async () => {
3876
3879
  const question = input.value;
@@ -3878,6 +3881,7 @@
3878
3881
  input.focus();
3879
3882
  return;
3880
3883
  }
3884
+ panel?.classList.add('ember-chat-panel--active');
3881
3885
  answer.textContent = '';
3882
3886
  status.textContent = '';
3883
3887
  retry.hidden = true;
@@ -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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"}
@@ -5442,6 +5442,20 @@ body.overview-tab-active .dashboard-main > *:not(#overview-section) {
5442
5442
 
5443
5443
  /* SPEC-179 — Settings modal + sidebar Settings button (Agentic OS DNA) */
5444
5444
 
5445
+ .sidebar-tool-buttons {
5446
+ display: flex;
5447
+ gap: 0.4rem;
5448
+ }
5449
+
5450
+ .sidebar-tool-buttons .sidebar-settings-button {
5451
+ flex: 1 1 0;
5452
+ min-width: 0;
5453
+ margin-top: 0;
5454
+ padding: 0.45rem 0.3rem;
5455
+ justify-content: center;
5456
+ font-size: 0.68rem;
5457
+ }
5458
+
5445
5459
  .sidebar-settings-button {
5446
5460
  display: flex;
5447
5461
  align-items: center;
@@ -6388,20 +6402,47 @@ input:focus-visible,
6388
6402
 
6389
6403
  .ember-chat-panel__header {
6390
6404
  display: flex;
6405
+ flex-direction: column;
6391
6406
  align-items: center;
6392
- justify-content: space-between;
6393
- margin-bottom: 8px;
6407
+ gap: 10px;
6408
+ margin-bottom: 12px;
6394
6409
  }
6395
6410
 
6396
6411
  .ember-chat-panel__prefix {
6412
+ align-self: flex-start;
6397
6413
  color: var(--accent);
6398
6414
  font-size: 12px;
6399
6415
  letter-spacing: 0.04em;
6400
6416
  }
6401
6417
 
6402
6418
  .ember-chat-panel__avatar {
6403
- width: 48px;
6404
- height: 48px;
6419
+ width: 260px;
6420
+ height: 260px;
6421
+ transition: width 0.45s ease, height 0.45s ease;
6422
+ }
6423
+
6424
+ .ember-chat-panel--active .ember-chat-panel__header {
6425
+ flex-direction: row;
6426
+ align-items: flex-start;
6427
+ justify-content: space-between;
6428
+ }
6429
+
6430
+ .ember-chat-panel--active .ember-chat-panel__avatar {
6431
+ width: 104px;
6432
+ height: 104px;
6433
+ }
6434
+
6435
+ .ember-chat-panel:not(.ember-chat-panel--active) .ember-chat-panel__answer,
6436
+ .ember-chat-panel:not(.ember-chat-panel--active) .ember-chat-panel__status,
6437
+ .ember-chat-panel:not(.ember-chat-panel--active) .ember-chat-panel__retry {
6438
+ display: none;
6439
+ }
6440
+
6441
+ @media (prefers-reduced-motion: reduce) {
6442
+ .ember-chat-panel__avatar {
6443
+ transition: none;
6444
+ animation: none;
6445
+ }
6405
6446
  }
6406
6447
 
6407
6448
  .ember-chat-panel__answer {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emberAvatar.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberAvatar.test.d.ts","sourceRoot":"","sources":["../../../../../src/tests/units/dashboard/modules/emberAvatar.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { EMBER_STATES, emberStateToVisual, buildFlameWireframe, projectFlameVertex, emberRadiusFactor, emberSwayOffset, FLAME_TIP_Y, FLAME_BASE_Y, } from '../../../../dashboard/modules/emberAvatar.js';
3
+ describe('EMBER_STATES', () => {
4
+ it('exposes exactly the three chat-driven states in narrative order', () => {
5
+ expect(EMBER_STATES).toEqual(['idle', 'working', 'error']);
6
+ });
7
+ });
8
+ describe('emberStateToVisual', () => {
9
+ it('makes the working state lean, flicker and glow harder than idle', () => {
10
+ const idle = emberStateToVisual('idle');
11
+ const working = emberStateToVisual('working');
12
+ expect(working.swayAmount).toBeGreaterThan(idle.swayAmount);
13
+ expect(working.flicker).toBeGreaterThan(idle.flicker);
14
+ expect(working.glow).toBeGreaterThan(idle.glow);
15
+ });
16
+ it('paints the error state with a distinct colour token from idle', () => {
17
+ expect(emberStateToVisual('error').color).not.toBe(emberStateToVisual('idle').color);
18
+ });
19
+ });
20
+ describe('buildFlameWireframe', () => {
21
+ it('has one tip vertex plus a rings × meridians body', () => {
22
+ const { vertices } = buildFlameWireframe({ rings: 6, meridians: 8 });
23
+ expect(vertices).toHaveLength(1 + 6 * 8);
24
+ expect(vertices[0]).toEqual([0, FLAME_TIP_Y, 0]);
25
+ for (const vertex of vertices) {
26
+ expect(vertex).toHaveLength(3);
27
+ }
28
+ });
29
+ it('connects the tip, the meridians and the ring loops with edges', () => {
30
+ const { edges } = buildFlameWireframe({ rings: 6, meridians: 8 });
31
+ const tipSpokes = 8;
32
+ const verticalLines = (6 - 1) * 8;
33
+ const ringLoops = 6 * 8;
34
+ expect(edges).toHaveLength(tipSpokes + verticalLines + ringLoops);
35
+ });
36
+ });
37
+ describe('projectFlameVertex', () => {
38
+ const projection = { tilt: 0.12, distance: 4, scale: 50, centerX: 120, centerY: 120 };
39
+ it('is deterministic for the same inputs', () => {
40
+ const vertex = [0.5, 0.2, -0.3];
41
+ expect(projectFlameVertex(vertex, 1.1, 0.2, projection)).toEqual(projectFlameVertex(vertex, 1.1, 0.2, projection));
42
+ });
43
+ it('leans the tip far more than the base for a positive sway', () => {
44
+ const tip = [0, FLAME_TIP_Y, 0];
45
+ const base = [0, FLAME_BASE_Y, 0];
46
+ const tipShift = projectFlameVertex(tip, 0, 0.4, projection).x - projectFlameVertex(tip, 0, 0, projection).x;
47
+ const baseShift = projectFlameVertex(base, 0, 0.4, projection).x - projectFlameVertex(base, 0, 0, projection).x;
48
+ expect(tipShift).toBeGreaterThan(baseShift);
49
+ expect(baseShift).toBeCloseTo(0, 6);
50
+ });
51
+ });
52
+ describe('emberRadiusFactor', () => {
53
+ it('returns the neutral factor of 1 at time 0', () => {
54
+ expect(emberRadiusFactor(emberStateToVisual('idle'), 0)).toBeCloseTo(1, 5);
55
+ });
56
+ it('stays within the flicker envelope', () => {
57
+ const visual = emberStateToVisual('working');
58
+ for (const time of [120, 900, 1750, 5000]) {
59
+ const factor = emberRadiusFactor(visual, time);
60
+ expect(factor).toBeGreaterThanOrEqual(1 - visual.flicker - 1e-6);
61
+ expect(factor).toBeLessThanOrEqual(1 + visual.flicker + 1e-6);
62
+ }
63
+ });
64
+ });
65
+ describe('emberSwayOffset', () => {
66
+ it('is zero at time 0 and bounded by the sway amplitude', () => {
67
+ const visual = emberStateToVisual('working');
68
+ expect(emberSwayOffset(visual, 0)).toBeCloseTo(0, 6);
69
+ for (const time of [200, 1234, 4321]) {
70
+ expect(Math.abs(emberSwayOffset(visual, time))).toBeLessThanOrEqual(visual.swayAmount + 1e-6);
71
+ }
72
+ });
73
+ });
74
+ //# sourceMappingURL=emberAvatar.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberAvatar.test.js","sourceRoot":"","sources":["../../../../../src/tests/units/dashboard/modules/emberAvatar.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emberAvatarRenderer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberAvatarRenderer.test.d.ts","sourceRoot":"","sources":["../../../../../src/tests/units/dashboard/modules/emberAvatarRenderer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mountEmberAvatar } from '../../../../dashboard/modules/emberAvatarRenderer.js';
3
+ function fakeCanvas() {
4
+ return { width: 240, height: 240, getContext: () => null };
5
+ }
6
+ describe('mountEmberAvatar (lifecycle glue)', () => {
7
+ it('schedules an animation frame on mount', () => {
8
+ const requested = [];
9
+ mountEmberAvatar({
10
+ canvas: fakeCanvas(),
11
+ requestFrame: (callback) => {
12
+ requested.push(callback);
13
+ return requested.length;
14
+ },
15
+ cancelFrame: () => undefined,
16
+ now: () => 0,
17
+ });
18
+ expect(requested).toHaveLength(1);
19
+ });
20
+ it('cancels the scheduled frame on destroy so the loop cannot leak', () => {
21
+ const cancelled = [];
22
+ const controls = mountEmberAvatar({
23
+ canvas: fakeCanvas(),
24
+ requestFrame: () => 42,
25
+ cancelFrame: (handle) => {
26
+ cancelled.push(handle);
27
+ },
28
+ now: () => 0,
29
+ });
30
+ controls.destroy();
31
+ expect(cancelled).toEqual([42]);
32
+ });
33
+ it('exposes setState and destroy controls', () => {
34
+ const controls = mountEmberAvatar({
35
+ canvas: fakeCanvas(),
36
+ requestFrame: () => 1,
37
+ cancelFrame: () => undefined,
38
+ now: () => 0,
39
+ });
40
+ expect(typeof controls.setState).toBe('function');
41
+ expect(typeof controls.destroy).toBe('function');
42
+ });
43
+ });
44
+ //# sourceMappingURL=emberAvatarRenderer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberAvatarRenderer.test.js","sourceRoot":"","sources":["../../../../../src/tests/units/dashboard/modules/emberAvatarRenderer.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAQ9E,SAAS,UAAU;IACjB,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;AAC7D,CAAC;AAED,QAAQ,CAAC,mCAAmC,EAAE,GAAG,EAAE;IACjD,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,SAAS,GAAkC,EAAE,CAAC;QAEpD,gBAAgB,CAAC;YACf,MAAM,EAAE,UAAU,EAAE;YACpB,YAAY,EAAE,CAAC,QAAQ,EAAE,EAAE;gBACzB,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACzB,OAAO,SAAS,CAAC,MAAM,CAAC;YAC1B,CAAC;YACD,WAAW,EAAE,GAAG,EAAE,CAAC,SAAS;YAC5B,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;SACb,CAAC,CAAC;QAEH,MAAM,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,SAAS,GAAa,EAAE,CAAC;QAE/B,MAAM,QAAQ,GAAG,gBAAgB,CAAC;YAChC,MAAM,EAAE,UAAU,EAAE;YACpB,YAAY,EAAE,GAAG,EAAE,CAAC,EAAE;YACtB,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE;gBACtB,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACzB,CAAC;YACD,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;SACb,CAAC,CAAC;QAEH,QAAQ,CAAC,OAAO,EAAE,CAAC;QAEnB,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,QAAQ,GAAG,gBAAgB,CAAC;YAChC,MAAM,EAAE,UAAU,EAAE;YACpB,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;YACrB,WAAW,EAAE,GAAG,EAAE,CAAC,SAAS;YAC5B,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;SACb,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAClD,MAAM,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviewflow",
3
- "version": "3.29.0",
3
+ "version": "3.30.0",
4
4
  "description": "AI-powered code review automation for GitLab/GitHub using Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/main/server.js",