@hanzlaa/rcode 4.1.1 → 4.3.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 (105) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +3 -0
  4. package/cli/agent.js +3 -1
  5. package/cli/index.js +29 -0
  6. package/cli/install.js +233 -15
  7. package/cli/lib/config.cjs +4 -2
  8. package/cli/lib/fsutil.cjs +13 -2
  9. package/cli/lib/homedir.cjs +21 -0
  10. package/cli/lib/schemas.cjs +6 -1
  11. package/cli/nuke.js +13 -8
  12. package/cli/postinstall.js +14 -4
  13. package/cli/rcode-slash-router.cjs +118 -0
  14. package/cli/uninstall.js +59 -1
  15. package/cli/update.js +10 -5
  16. package/cli/workflow.js +3 -1
  17. package/dist/rcode.js +241 -227
  18. package/package.json +1 -1
  19. package/rcode/bin/rcode-tools.cjs +15 -6
  20. package/rcode/commands/scaffold-project.md +2 -2
  21. package/rcode/skills/actions/2-plan/rcode-create-epics-and-stories/steps/step-04-final-validation.md +1 -1
  22. package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/README.md +2 -2
  23. package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/step-09-state-sync.md +1 -1
  24. package/rcode/skills/actions/4-implementation/rcode-code-review/steps/step-02-review.md +1 -1
  25. package/rcode/skills/actions/4-implementation/rcode-git-flow/SKILL.md +1 -1
  26. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/SKILL.md +39 -12
  27. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-01-target.md +18 -3
  28. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-02-safety.md +27 -3
  29. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-brownfield.md +57 -0
  30. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-clone.md +4 -1
  31. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-04-post-setup.md +15 -1
  32. package/rcode/skills/actions/4-implementation/rcode-trim/SKILL.md +1 -1
  33. package/rcode/workflows/audit-milestone.md +1 -1
  34. package/rcode/workflows/discuss-phase.md +1 -1
  35. package/rcode/workflows/execute-milestone.md +1 -1
  36. package/rcode/workflows/execute-regression-gates.md +3 -0
  37. package/rcode/workflows/execute-sprint.md +27 -1
  38. package/rcode/workflows/execute-waves.md +6 -0
  39. package/rcode/workflows/execute.md +13 -3
  40. package/rcode/workflows/new-milestone.md +2 -2
  41. package/rcode/workflows/new-project.md +4 -0
  42. package/rcode/workflows/plan-research-validation.md +1 -1
  43. package/rcode/workflows/plan-spawn-planner.md +2 -2
  44. package/rcode/workflows/plan.md +34 -15
  45. package/rcode/workflows/review.md +2 -0
  46. package/rcode/workflows/scaffold-project.md +5 -1
  47. package/rcode/workflows/session-report.md +1 -1
  48. package/rcode/workflows/ship.md +39 -0
  49. package/rcode/workflows/sprint-planning.md +27 -0
  50. package/rcode/workflows/status.md +3 -3
  51. package/server/dashboard.js +26 -7
  52. package/server/lib/api.js +62 -4
  53. package/server/lib/html/client/agents-data.js +22 -18
  54. package/server/lib/html/client/app.js +3 -0
  55. package/server/lib/html/client/components/AgentCard.js +127 -0
  56. package/server/lib/html/client/components/App.js +104 -39
  57. package/server/lib/html/client/components/CommandPalette.js +133 -0
  58. package/server/lib/html/client/components/FileReader.js +116 -0
  59. package/server/lib/html/client/components/FilterChips.js +94 -0
  60. package/server/lib/html/client/components/NotifyCenter.js +117 -0
  61. package/server/lib/html/client/components/OrchPanel.js +80 -52
  62. package/server/lib/html/client/components/PhaseGraph.js +300 -0
  63. package/server/lib/html/client/components/RejectDialog.js +78 -0
  64. package/server/lib/html/client/components/RunnerPicker.js +190 -0
  65. package/server/lib/html/client/components/Sidebar.js +106 -61
  66. package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
  67. package/server/lib/html/client/components/TaskPipeline.js +83 -0
  68. package/server/lib/html/client/components/Topbar.js +86 -39
  69. package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
  70. package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
  71. package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
  72. package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
  73. package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
  74. package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
  75. package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
  76. package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
  77. package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
  78. package/server/lib/html/client/components/shared.js +47 -11
  79. package/server/lib/html/client/filter-state.js +72 -0
  80. package/server/lib/html/client/icons-client.js +7 -0
  81. package/server/lib/html/client/notify.js +75 -0
  82. package/server/lib/html/client/orchestrator.js +168 -41
  83. package/server/lib/html/client/preact.js +13 -8
  84. package/server/lib/html/client/store.js +70 -6
  85. package/server/lib/html/client/util.js +78 -0
  86. package/server/lib/html/client/vendor/htm.js +1 -0
  87. package/server/lib/html/client/vendor/preact-hooks.js +2 -0
  88. package/server/lib/html/client/vendor/preact.js +2 -0
  89. package/server/lib/html/client/views/AgentsView.js +144 -51
  90. package/server/lib/html/client/views/FilesView.js +20 -103
  91. package/server/lib/html/client/views/KanbanView.js +40 -21
  92. package/server/lib/html/client/views/MemoryView.js +26 -9
  93. package/server/lib/html/client/views/MilestonesView.js +4 -4
  94. package/server/lib/html/client/views/OrchestrationView.js +154 -19
  95. package/server/lib/html/client/views/OverviewView.js +47 -239
  96. package/server/lib/html/client/views/PhasesView.js +50 -6
  97. package/server/lib/html/client/views/RoadmapView.js +6 -3
  98. package/server/lib/html/client/views/SprintsView.js +50 -6
  99. package/server/lib/html/client/views/TasksView.js +4 -3
  100. package/server/lib/html/client.js +21 -4
  101. package/server/lib/html/css.js +2761 -8
  102. package/server/lib/html/icons.js +7 -0
  103. package/server/lib/html/shell.js +10 -3
  104. package/server/lib/scanner.js +376 -39
  105. package/server/orchestrator.js +329 -5
@@ -0,0 +1,78 @@
1
+ /**
2
+ * RejectDialog — structured rejection dialog for waiting checkpoint sessions.
3
+ *
4
+ * Props:
5
+ * session — OrchCard session object ({ storyId, phase?, ... })
6
+ * onClose — callback invoked on cancel, backdrop click, Escape, or after
7
+ * a successful submission.
8
+ *
9
+ * Rules:
10
+ * - Submit button is disabled until a non-empty reason is typed (GATE-1).
11
+ * - Uses showToast() for post-submit feedback; browser dialogs are forbidden.
12
+ * - All visuals are driven by CSS classes (no style attribute).
13
+ */
14
+
15
+ import { html, useState, useEffect } from '../preact.js';
16
+ import { submitRejection } from '../orchestrator.js';
17
+ import { showToast } from './shared.js';
18
+
19
+ export function RejectDialog({ session, onClose }) {
20
+ const [reason, setReason] = useState('');
21
+ const [busy, setBusy] = useState(false);
22
+
23
+ const trimmed = reason.trim();
24
+ const disabled = !trimmed || busy;
25
+
26
+ // Escape-to-close
27
+ useEffect(() => {
28
+ function handleKey(e) {
29
+ if (e.key === 'Escape') onClose();
30
+ }
31
+ document.addEventListener('keydown', handleKey);
32
+ return () => document.removeEventListener('keydown', handleKey);
33
+ }, [onClose]);
34
+
35
+ function handleSubmit() {
36
+ if (disabled) return;
37
+ setBusy(true);
38
+ submitRejection(session.storyId, trimmed, session.phase || null)
39
+ .then(d => {
40
+ if (d && d.ok) {
41
+ showToast('Rejection recorded');
42
+ onClose();
43
+ } else {
44
+ showToast('Reject failed: ' + ((d && d.error) || 'unknown'));
45
+ setBusy(false);
46
+ }
47
+ })
48
+ .catch(() => {
49
+ showToast('Could not reach orchestrator');
50
+ setBusy(false);
51
+ });
52
+ }
53
+
54
+ return html`
55
+ <div class="reject-overlay" onClick=${onClose}>
56
+ <div class="reject-dialog" onClick=${e => e.stopPropagation()}>
57
+ <div class="reject-dialog-title">
58
+ Reject checkpoint — ${session.storyId}
59
+ </div>
60
+ <textarea
61
+ class="reject-dialog-input"
62
+ placeholder="Why is this checkpoint being rejected? (required)"
63
+ value=${reason}
64
+ onInput=${e => setReason(e.target.value)}
65
+ autofocus
66
+ ></textarea>
67
+ <div class="reject-dialog-actions">
68
+ <button class="reject-cancel" onClick=${onClose}>Cancel</button>
69
+ <button
70
+ class="reject-submit"
71
+ disabled=${disabled}
72
+ onClick=${handleSubmit}
73
+ >${busy ? 'Recording…' : 'Submit rejection'}</button>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ `;
78
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * RunnerPicker — anchored popover for choosing which agent CLI + model a
3
+ * Run button launches.
4
+ *
5
+ * One instance is mounted in App.js; every Run button opens it via
6
+ * openRunnerPicker(anchorEl, run). State lives in store.runnerPicker:
7
+ * { open, x, y, run: { kind: 'session'|'command', storyId?, cmd, title? } }
8
+ *
9
+ * kind 'session' → runAndOpenTerm(storyId, cmd, title, { runner, model })
10
+ * kind 'command' → runCommandFromUI(cmd, { runner, model })
11
+ *
12
+ * Runner list comes from GET /api/runners (fetchRunners, cached). Runners are
13
+ * rendered as an option list (not a <select>) so each row can carry a "Beta"
14
+ * pill (every CLI except claude) and unavailable ones can show their server-
15
+ * reported reason ('not installed' / 'untested flags') as a disabled tooltip.
16
+ * A runner with an empty models[] gets no model dropdown at all. The last
17
+ * confirmed runner + model are remembered in localStorage and preselected.
18
+ * Esc and click-outside close the popover. The server re-validates runner
19
+ * and model on POST /api/run — this UI is convenience, not the boundary.
20
+ *
21
+ * Positioning uses CSS custom properties (--rp-x/--rp-y) set via the element
22
+ * ref, never an inline style attribute; the popover clamps to the viewport.
23
+ */
24
+
25
+ import { html, useState, useEffect, useRef } from '../preact.js';
26
+ import { useStore, setState } from '../store.js';
27
+ import { fetchRunners, runAndOpenTerm, runCommandFromUI } from '../orchestrator.js';
28
+
29
+ const PREF_KEY = 'majlis-runner-pref';
30
+
31
+ function loadPref() {
32
+ try { return JSON.parse(localStorage.getItem(PREF_KEY)) || {}; } catch { return {}; }
33
+ }
34
+
35
+ function savePref(runner, model) {
36
+ try { localStorage.setItem(PREF_KEY, JSON.stringify({ runner, model })); } catch { /* private mode */ }
37
+ }
38
+
39
+ /**
40
+ * Open the picker anchored under anchorEl.
41
+ * @param {Element} anchorEl — the clicked Run button
42
+ * @param {{ kind: 'session'|'command', storyId?: string, cmd: string, title?: string }} run
43
+ */
44
+ export function openRunnerPicker(anchorEl, run) {
45
+ const r = anchorEl && anchorEl.getBoundingClientRect
46
+ ? anchorEl.getBoundingClientRect()
47
+ : { left: 24, bottom: 24 };
48
+ setState({
49
+ runnerPicker: { open: true, x: Math.round(r.left), y: Math.round(r.bottom + 6), run },
50
+ });
51
+ }
52
+
53
+ export function closeRunnerPicker() {
54
+ setState({ runnerPicker: null });
55
+ }
56
+
57
+ /** Preselect the remembered runner/model when valid, else claude, else the first installed CLI. */
58
+ function initialSelection(runners) {
59
+ const pref = loadPref();
60
+ const valid = id => runners.some(r => r.id === id && r.available);
61
+ const runnerId = valid(pref.runner) ? pref.runner
62
+ : valid('claude') ? 'claude'
63
+ : (runners.find(r => r.available) || {}).id || '';
64
+ const entry = runners.find(r => r.id === runnerId);
65
+ const model = (entry && pref.runner === runnerId && entry.models.includes(pref.model))
66
+ ? pref.model : '';
67
+ return { runnerId, model };
68
+ }
69
+
70
+ export function RunnerPicker() {
71
+ const picker = useStore(s => s.runnerPicker);
72
+ const open = !!(picker && picker.open);
73
+
74
+ const [runners, setRunners ] = useState(null); // null = loading, [] = unreachable
75
+ const [runnerId, setRunnerId] = useState('');
76
+ const [model, setModel ] = useState('');
77
+ const ref = useRef(null);
78
+
79
+ // Load the runner list and (re)apply the remembered selection on each open.
80
+ useEffect(() => {
81
+ if (!open) return;
82
+ let alive = true;
83
+ fetchRunners().then(list => {
84
+ if (!alive) return;
85
+ setRunners(list);
86
+ const sel = initialSelection(list);
87
+ setRunnerId(sel.runnerId);
88
+ setModel(sel.model);
89
+ });
90
+ return () => { alive = false; };
91
+ }, [open]);
92
+
93
+ // Esc / click-outside close. mousedown fires after the opening click's
94
+ // event cycle, so the click that opened the picker never closes it.
95
+ useEffect(() => {
96
+ if (!open) return;
97
+ function onKey(e) { if (e.key === 'Escape') closeRunnerPicker(); }
98
+ function onDown(e) {
99
+ if (ref.current && !ref.current.contains(e.target)) closeRunnerPicker();
100
+ }
101
+ document.addEventListener('keydown', onKey);
102
+ document.addEventListener('mousedown', onDown);
103
+ return () => {
104
+ document.removeEventListener('keydown', onKey);
105
+ document.removeEventListener('mousedown', onDown);
106
+ };
107
+ }, [open]);
108
+
109
+ // Anchor below the Run button, clamped to the viewport. CSS vars (not an
110
+ // inline style attribute) carry the coordinates into the stylesheet.
111
+ useEffect(() => {
112
+ if (!open || !ref.current) return;
113
+ const el = ref.current;
114
+ const x = Math.max(8, Math.min(picker.x, window.innerWidth - el.offsetWidth - 8));
115
+ const y = Math.max(8, Math.min(picker.y, window.innerHeight - el.offsetHeight - 8));
116
+ el.style.setProperty('--rp-x', x + 'px');
117
+ el.style.setProperty('--rp-y', y + 'px');
118
+ }, [open, picker, runners]);
119
+
120
+ if (!open) return null;
121
+
122
+ const entry = (runners || []).find(r => r.id === runnerId) || null;
123
+ const models = (entry && entry.models) || [];
124
+
125
+ function handleRunnerSelect(id) {
126
+ setRunnerId(id);
127
+ setModel(''); // model lists differ per runner — reset to CLI default
128
+ }
129
+
130
+ function handleRun() {
131
+ const run = picker.run || {};
132
+ savePref(runnerId, model);
133
+ closeRunnerPicker();
134
+ const opts = { runner: runnerId, model };
135
+ if (run.kind === 'command') {
136
+ runCommandFromUI(run.cmd, opts);
137
+ } else {
138
+ runAndOpenTerm(run.storyId, run.cmd, run.title || run.storyId, opts);
139
+ }
140
+ }
141
+
142
+ return html`
143
+ <div class="runner-picker" ref=${ref} role="dialog" aria-label="Choose runner and model"
144
+ onClick=${e => e.stopPropagation()}>
145
+ <div class="runner-picker-title">
146
+ Run ${(picker.run && picker.run.title) || ''}
147
+ </div>
148
+ ${runners === null ? html`
149
+ <div class="runner-picker-hint">Detecting installed CLIs…</div>
150
+ ` : runners.length === 0 ? html`
151
+ <div class="runner-picker-hint">Orchestrator unreachable — cannot list runners.</div>
152
+ ` : html`
153
+ <div class="runner-picker-field">
154
+ <span class="runner-picker-label" id="runner-picker-cli-label">Agent CLI</span>
155
+ <div class="runner-picker-list" role="listbox" aria-labelledby="runner-picker-cli-label">
156
+ ${runners.map(r => html`
157
+ <button key=${r.id} type="button" role="option"
158
+ aria-selected=${r.id === runnerId}
159
+ class=${'runner-picker-option' + (r.id === runnerId ? ' selected' : '')}
160
+ disabled=${!r.available}
161
+ title=${r.available ? r.label : (r.reason || 'not installed')}
162
+ onClick=${() => handleRunnerSelect(r.id)}>
163
+ <span class="runner-picker-option-label">${r.label}</span>
164
+ ${r.beta ? html`<span class="runner-beta-pill">Beta</span>` : null}
165
+ ${!r.available ? html`
166
+ <span class="runner-picker-option-hint">${r.reason || 'not installed'}</span>
167
+ ` : null}
168
+ </button>
169
+ `)}
170
+ </div>
171
+ </div>
172
+ ${models.length ? html`
173
+ <label class="runner-picker-field">
174
+ <span class="runner-picker-label">Model</span>
175
+ <select class="runner-picker-select" value=${model}
176
+ onChange=${e => setModel(e.target.value)}>
177
+ <option value="">default</option>
178
+ ${models.map(m => html`<option key=${m} value=${m}>${m}</option>`)}
179
+ </select>
180
+ </label>
181
+ ` : null}
182
+ `}
183
+ <div class="runner-picker-actions">
184
+ <button class="runner-picker-btn" onClick=${closeRunnerPicker}>Cancel</button>
185
+ <button class="runner-picker-btn runner-picker-btn--run"
186
+ disabled=${!runnerId} onClick=${handleRun}>▶ Run</button>
187
+ </div>
188
+ </div>
189
+ `;
190
+ }
@@ -1,86 +1,131 @@
1
1
  /**
2
- * Sidebar component — project label, nav sections, 12 nav-link buttons.
2
+ * Sidebar component — redesigned chrome to match the mockup.
3
3
  *
4
- * Reuses existing CSS classes from css.js: sidebar, nav-section, nav-link,
5
- * data-view, active. Emoji replaced with SVG icons from icons-client.js.
4
+ * Public API is UNCHANGED App.js still calls:
5
+ * <${Sidebar} activeView=${view} projectName=${storeState.projectName} />
6
+ *
7
+ * Layout (top → bottom):
8
+ * 1. rcode logo badge
9
+ * 2. project switcher (shows project.name)
10
+ * 3. vertical nav with per-item icons (Overview active by default)
11
+ * 4. Project Health mini-card (ProjectHealth.js)
12
+ * 5. user profile footer (avatar initials + name + email)
13
+ *
14
+ * Reads `project { name, user { name, email } }` from the store; falls back to
15
+ * the projectName prop. No sample data — when no user is configured the
16
+ * profile footer is hidden, and a missing project name shows "No project".
17
+ * No inline style= attributes — all styling via .sb-* classes in css.js.
6
18
  */
7
19
 
8
20
  import { html } from '../preact.js';
9
21
  import { Icon } from '../icons-client.js';
10
22
  import { useStore } from '../store.js';
11
- import { allSprints, allTasks } from '../util.js';
12
- import { AGENTS } from '../agents-data.js';
23
+ import { ProjectHealth } from './dashboard/ProjectHealth.js';
13
24
 
14
- // Nav structure: [ { section, links: [ { view, icon, label } ] } ]
15
- const NAV_SECTIONS = [
16
- {
17
- section: 'Overview',
18
- links: [
19
- { view: 'overview', icon: 'home', label: 'Overview' },
20
- { view: 'orchestration', icon: 'activity', label: 'Orchestration' },
21
- { view: 'roadmap', icon: 'map', label: 'Roadmap' },
22
- ],
23
- },
24
- {
25
- section: 'Planning',
26
- links: [
27
- { view: 'milestones', icon: 'target', label: 'Milestones' },
28
- { view: 'phases', icon: 'layers', label: 'Phases' },
29
- { view: 'sprints', icon: 'zap', label: 'Sprints' },
30
- { view: 'tasks', icon: 'checkSquare', label: 'Tasks' },
31
- { view: 'kanban', icon: 'kanban', label: 'Kanban' },
32
- ],
33
- },
34
- {
35
- section: 'Workspace',
36
- links: [
37
- { view: 'files', icon: 'file', label: 'Files' },
38
- { view: 'agents', icon: 'users', label: 'Agents' },
39
- { view: 'decisions', icon: 'scale', label: 'Decisions' },
40
- { view: 'memory', icon: 'database', label: 'Memory' },
41
- ],
42
- },
25
+ // Single flat nav one entry per real Preact view (PREACT_VIEWS in App.js).
26
+ // Keep this list in sync with App.js: a view key absent there silently
27
+ // falls back to Overview, so never add a link without a matching view.
28
+ const NAV_LINKS = [
29
+ { view: 'overview', icon: 'home', label: 'Overview' },
30
+ { view: 'roadmap', icon: 'map', label: 'Roadmap' },
31
+ { view: 'milestones', icon: 'flag', label: 'Milestones' },
32
+ { view: 'phases', icon: 'layers', label: 'Phases' },
33
+ { view: 'sprints', icon: 'zap', label: 'Sprints' },
34
+ { view: 'tasks', icon: 'checkSquare', label: 'Tasks' },
35
+ { view: 'kanban', icon: 'kanban', label: 'Kanban' },
36
+ { view: 'decisions', icon: 'scale', label: 'Decisions' },
37
+ { view: 'files', icon: 'file-text', label: 'Files' },
38
+ { view: 'agents', icon: 'users', label: 'Agents' },
39
+ { view: 'memory', icon: 'brain', label: 'Memory' },
40
+ { view: 'orchestration', icon: 'terminal', label: 'Orchestration' },
43
41
  ];
44
42
 
43
+ /** Two-letter initials from a display name. */
44
+ function initials(name) {
45
+ const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
46
+ if (!parts.length) return '?';
47
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
48
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
49
+ }
50
+
45
51
  /**
46
52
  * Sidebar component.
47
53
  *
48
54
  * Props:
49
- * activeView {string} — currently active view key
50
- * projectName {string} — displayed under the "rcode" label
55
+ * activeView {string} — currently active view key (drives nav highlight)
56
+ * projectName {string} — fallback project name when store has no project slice
51
57
  */
52
58
  export function Sidebar({ activeView, projectName }) {
53
- const S = useStore();
54
- const counts = {
55
- phases: (S.phases || []).length,
56
- sprints: allSprints(S.phases).length,
57
- tasks: allTasks(S.phases).length,
58
- decisions: (S.decisions || []).length,
59
- agents: AGENTS.length,
60
- };
59
+ // Slice subscription — Sidebar only re-renders when the project slice
60
+ // changes, not on every refreshing/sessions tick.
61
+ const project = useStore(s => s.project) || {};
62
+ const name = project.name || projectName || 'No project';
63
+ const user = (project.user && project.user.name) ? project.user : null;
64
+
65
+ // Full store subscription for live health badge counts.
66
+ // Re-renders on every setState (sessions poll every 4 s, state refresh every 30 s).
67
+ const { activeSessions, blockers } = useStore();
68
+ const sessionCount = (activeSessions || []).filter(s => s.status === 'running').length;
69
+ const blockerCount = (blockers || []).length;
61
70
 
62
71
  return html`
63
72
  <aside class="sidebar" id="sidebar">
64
- <div class="sidebar-project">
65
- <div class="project-label">rcode</div>
66
- <span>${projectName || ''}</span>
73
+ <div class="sb-logo">
74
+ <span class="sb-logo-badge">r</span>
75
+ <span class="sb-logo-word">rcode</span>
76
+ </div>
77
+
78
+ <!-- Visually inert: the server scans exactly one project, so there is
79
+ no switcher menu. Rendered as a plain label, no chevron/affordance. -->
80
+ <div class="sb-switcher sb-switcher--static" title=${name}>
81
+ <span class="sb-switcher-dot"></span>
82
+ <span class="sb-switcher-name">${name}</span>
67
83
  </div>
68
- <nav>
69
- ${NAV_SECTIONS.map(({ section, links }) => html`
70
- <div class="nav-section">${section}</div>
71
- ${links.map(({ view, icon, label }) => html`
72
- <button
73
- class=${'nav-link' + (activeView === view ? ' active' : '')}
74
- data-view=${view}
75
- onClick=${() => { location.hash = view; }}
76
- >
77
- <${Icon} name=${icon} size=${14} />
78
- ${' ' + label}
79
- ${counts[view] ? html`<span class="nav-count">${counts[view]}</span>` : null}
80
- </button>
81
- `)}
84
+
85
+ <div class="sidebar-health">
86
+ <span
87
+ class=${'health-badge' + (sessionCount === 0 ? ' health-badge--zero' : '')}
88
+ title=${sessionCount + ' active orchestration session' + (sessionCount === 1 ? '' : 's')}
89
+ >
90
+ <${Icon} name="activity" size=${12} />
91
+ ${sessionCount} active
92
+ </span>
93
+ <span
94
+ class=${'health-badge' + (blockerCount > 0 ? ' health-badge--alert' : ' health-badge--zero')}
95
+ title=${blockerCount + ' blocker' + (blockerCount === 1 ? '' : 's')}
96
+ >
97
+ <${Icon} name="alert-triangle" size=${12} />
98
+ ${blockerCount} blocked
99
+ </span>
100
+ </div>
101
+
102
+ <nav class="sb-nav">
103
+ ${NAV_LINKS.map(({ view, icon, label }) => html`
104
+ <button
105
+ class=${'sb-nav-link' + (activeView === view ? ' active' : '')}
106
+ data-view=${view}
107
+ aria-current=${activeView === view ? 'page' : undefined}
108
+ onClick=${() => { location.hash = view; }}
109
+ >
110
+ <span class="sb-nav-ic"><${Icon} name=${icon} size=${16} /></span>
111
+ <span class="sb-nav-label">${label}</span>
112
+ </button>
82
113
  `)}
83
114
  </nav>
115
+
116
+ <div class="sb-health">
117
+ <${ProjectHealth} />
118
+ </div>
119
+
120
+ ${user ? html`
121
+ <div class="sb-profile">
122
+ <span class="sb-avatar">${initials(user.name)}</span>
123
+ <span class="sb-profile-meta">
124
+ <span class="sb-profile-name">${user.name}</span>
125
+ <span class="sb-profile-email">${user.email || ''}</span>
126
+ </span>
127
+ </div>
128
+ ` : null}
84
129
  </aside>
85
130
  `;
86
131
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * StatusSummaryBar — aggregate count chips for phases, sprints, and sessions
3
+ * grouped by status. Reads from the shared store; renders a flex row of
4
+ * labelled count chips.
5
+ *
6
+ * No props required — all data comes from useStore().
7
+ * Rendered above views in 34.2 once wired into App.js.
8
+ */
9
+
10
+ import { html } from '../preact.js';
11
+ import { useStore } from '../store.js';
12
+ import { allSprints, chip } from '../util.js';
13
+
14
+ /**
15
+ * Build a `{ [cls]: count }` map from an array of items by normalising each
16
+ * item's status through `chip()` and incrementing the corresponding cls bucket.
17
+ *
18
+ * @param {Array<{ status: string }>} items
19
+ * @returns {Object.<string, number>}
20
+ */
21
+ function countByStatus(items) {
22
+ const map = {};
23
+ for (const item of items) {
24
+ const { cls } = chip(item.status || '');
25
+ map[cls] = (map[cls] || 0) + 1;
26
+ }
27
+ return map;
28
+ }
29
+
30
+ /**
31
+ * Render a single group of count chips.
32
+ *
33
+ * @param {{ label: string, counts: Object.<string, number> }} props
34
+ */
35
+ function SummaryGroup({ label, counts }) {
36
+ const entries = Object.entries(counts).filter(([, n]) => n > 0);
37
+ if (entries.length === 0) return null;
38
+ return html`
39
+ <div class="summary-group">
40
+ <span class="summary-group-label">${label}</span>
41
+ ${entries.map(([cls, count]) => html`
42
+ <span class=${'summary-count-chip ' + cls}>${count} ${cls}</span>
43
+ `)}
44
+ </div>
45
+ `;
46
+ }
47
+
48
+ /**
49
+ * StatusSummaryBar — row of count chips for phases, sprints, and sessions.
50
+ * Suppresses a group entirely when its source array is empty.
51
+ */
52
+ export function StatusSummaryBar() {
53
+ const S = useStore();
54
+
55
+ const phases = S.phases || [];
56
+ const sprints = allSprints(phases);
57
+ const sessions = S.activeSessions || [];
58
+
59
+ const phaseCounts = countByStatus(phases);
60
+ const sprintCounts = countByStatus(sprints);
61
+ const sessionCounts = countByStatus(sessions);
62
+
63
+ const hasPhases = phases.length > 0;
64
+ const hasSprints = sprints.length > 0;
65
+ const hasSessions = sessions.length > 0;
66
+
67
+ if (!hasPhases && !hasSprints && !hasSessions) return null;
68
+
69
+ return html`
70
+ <div class="summary-bar">
71
+ ${hasPhases ? html`<${SummaryGroup} label="Phases" counts=${phaseCounts} />` : null}
72
+ ${hasSprints ? html`<${SummaryGroup} label="Sprints" counts=${sprintCounts} />` : null}
73
+ ${hasSessions ? html`<${SummaryGroup} label="Sessions" counts=${sessionCounts} />` : null}
74
+ </div>
75
+ `;
76
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * TaskPipeline — compact horizontal stage stepper for a single task.
3
+ *
4
+ * Stages derive from the rcode story lifecycle as seen by the scanner
5
+ * (todo/planned → in_progress/active → review/verify → done/completed):
6
+ * Planned → In Progress → Review → Done.
7
+ * "blocked" is not a stage — it pins the stepper at In Progress and adds
8
+ * a red "Blocked" badge (and a red ring on the current node).
9
+ *
10
+ * Tolerant of sparse data: a bare status string is enough; overview
11
+ * `tasks.inProgress` rows have only { title, pct }, so pct is used as a
12
+ * fallback. Unknown/missing status renders as Planned — never crashes.
13
+ *
14
+ * Props:
15
+ * task — { status?, pct?, title? } (anything else ignored)
16
+ * mini — smaller variant for overview card rows
17
+ * running — a live orchestrator session exists for this task; pins the
18
+ * stepper at In Progress (unless further along) and pulses the
19
+ * current node so live work is visible at a glance
20
+ */
21
+
22
+ import { html } from '../preact.js';
23
+
24
+ const STAGES = ['Planned', 'In Progress', 'Review', 'Done'];
25
+
26
+ /** Current stage index (0–3) for a task; 3 means fully done. */
27
+ export function taskStageIndex(task) {
28
+ const s = String((task && task.status) || '').toLowerCase();
29
+ if (/done|complete|shipped/.test(s)) return 3;
30
+ if (/review|verif|uat/.test(s)) return 2;
31
+ if (/active|progress|running|blocked/.test(s)) return 1;
32
+ if (!s && task && Number.isFinite(task.pct)) {
33
+ return task.pct >= 100 ? 3 : task.pct > 0 ? 1 : 0;
34
+ }
35
+ return 0;
36
+ }
37
+
38
+ export function TaskPipeline({ task, mini, running }) {
39
+ const t = task || {};
40
+ // A live session means work is happening NOW — never show it as Planned,
41
+ // but don't demote a task already at Review/Done.
42
+ const cur = running ? Math.max(taskStageIndex(t), 1) : taskStageIndex(t);
43
+ const blocked = /blocked/i.test(String(t.status || ''));
44
+ // Number of fully-completed stages. When the task is done the Done node
45
+ // itself is filled, so all four count as complete.
46
+ const doneCount = cur === 3 ? 4 : cur;
47
+
48
+ const parts = [];
49
+ STAGES.forEach((label, i) => {
50
+ if (i > 0) {
51
+ const lineDone = i <= doneCount;
52
+ parts.push(html`
53
+ <span key=${'l' + i} class=${'tpipe-line' + (lineDone ? ' tpipe-line--done' : '')}></span>
54
+ `);
55
+ }
56
+ const isDone = i < doneCount;
57
+ const isCurrent = !isDone && i === cur;
58
+ let cls = 'tpipe-node';
59
+ let state = 'upcoming';
60
+ if (isDone) { cls += ' tpipe-node--done'; state = 'complete'; }
61
+ else if (isCurrent) {
62
+ cls += blocked ? ' tpipe-node--current tpipe-node--blocked' : ' tpipe-node--current';
63
+ if (running && !blocked && cur < 3) cls += ' tpipe-node--live';
64
+ state = blocked ? 'blocked' : 'current';
65
+ }
66
+ parts.push(html`
67
+ <span key=${'n' + i} class=${cls} title=${label + ' · ' + state}>
68
+ ${isDone ? '✓' : ''}
69
+ </span>
70
+ `);
71
+ });
72
+
73
+ const summary = blocked
74
+ ? 'Pipeline: blocked at ' + STAGES[cur]
75
+ : 'Pipeline: ' + (cur === 3 ? 'Done' : STAGES[cur]) + ' (stage ' + (Math.min(cur, 3) + 1) + ' of 4)';
76
+
77
+ return html`
78
+ <span class=${'tpipe' + (mini ? ' tpipe--mini' : '')} role="img" aria-label=${summary}>
79
+ ${parts}
80
+ ${blocked ? html`<span class="tpipe-blocked">Blocked</span>` : null}
81
+ </span>
82
+ `;
83
+ }