@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
@@ -1,83 +1,176 @@
1
1
  /**
2
- * AgentsView — Preact port of the #view-agents markup from shell.js.
2
+ * AgentsView — Team roster: category sections of rich agent cards plus an
3
+ * agent detail drawer (components/AgentCard.js).
3
4
  *
4
- * Renders Team (real agents) and AI Agents groups from the AGENTS roster
5
- * that was moved client-side into agents-data.js.
5
+ * Cards render from the client-side AGENTS roster (agents-data.js) plus a
6
+ * one-shot /api/agents call that returns frontmatter metadata (description,
7
+ * model, tools) per agent definition — small payload, so cards can show
8
+ * summaries and chips without touching prompt bodies.
6
9
  *
7
- * Clicking an agent sets store.requestedFile = skillSlug and navigates to
8
- * the Files view, replacing the legacy viewAgentSkill() setTimeout+DOM-poll
9
- * hack in shell.js. FilesView watches requestedFile and pre-fills the filter.
10
+ * Clicking a card opens the drawer with the agent's FULL prompt: the actual
11
+ * rcode/agents/<file> body fetched lazily through the existing /api/file
12
+ * handler and rendered as markdown. Prompts are fetched one at a time on
13
+ * click (never all at once) and cached per file for the session.
10
14
  */
11
15
 
12
- import { html, useState } from '../preact.js';
13
- import { setState } from '../store.js';
16
+ import { html, useState, useEffect, useCallback, useMemo } from '../preact.js';
17
+ import { AgentCard, AgentDrawer } from '../components/AgentCard.js';
14
18
  import { AGENTS } from '../agents-data.js';
15
19
 
16
- const REAL_AGENTS = AGENTS.filter(a => a.real);
17
- const AI_AGENTS = AGENTS.filter(a => !a.real);
20
+ // Category sections in display order. Roster `type` values not listed here
21
+ // fall into Specialists so a future type can't silently drop agents.
22
+ const SECTIONS = [
23
+ { label: 'Leadership', types: ['leadership'] },
24
+ { label: 'Engineering', types: ['engineering'] },
25
+ { label: 'Product', types: ['product'] },
26
+ { label: 'Design', types: ['design'] },
27
+ { label: 'Quality', types: ['quality'] },
28
+ { label: 'Specialists', types: ['support', 'system'] },
29
+ ];
30
+ const KNOWN_TYPES = SECTIONS.flatMap(s => s.types);
18
31
 
19
- // ---- Single agent card ----
20
- function AgentCard({ agent }) {
21
- const skillSlug = agent.name.split(' ')[0].toLowerCase();
22
-
23
- function handleClick() {
24
- // Navigate to Files view and pre-fill search with the agent's skill slug.
25
- // FilesView watches requestedFile in the store and responds via useEffect.
26
- setState({ requestedFile: skillSlug });
27
- window.location.hash = 'files';
28
- }
32
+ function agentsForSection(section) {
33
+ return AGENTS.filter(a =>
34
+ section.types.includes(a.type) ||
35
+ (section.label === 'Specialists' && !KNOWN_TYPES.includes(a.type))
36
+ );
37
+ }
29
38
 
30
- return html`
31
- <div
32
- class="agent-card"
33
- onClick=${handleClick}
34
- style="cursor:pointer;"
35
- >
36
- <div class="name">
37
- ${agent.name}
38
- ${agent.real ? html` <span class="real-badge">real</span>` : null}
39
- ${' '}<span class="type-badge">${agent.type}</span>
40
- </div>
41
- <div class="arabic">${agent.arabic}</div>
42
- <div class="role">${agent.role}</div>
43
- </div>
44
- `;
39
+ function matchesFilter(agent, meta, filter) {
40
+ if (!filter) return true;
41
+ const haystack = [
42
+ agent.name, agent.role, agent.arabic, agent.type,
43
+ meta && meta.model, meta && meta.description, meta && (meta.tools || []).join(' '),
44
+ ].filter(Boolean).join(' ');
45
+ return haystack.toLowerCase().includes(filter.toLowerCase());
45
46
  }
46
47
 
47
- // ---- Agent group ----
48
- function AgentGroup({ label, agents, filter }) {
49
- const visible = agents.filter(a => {
50
- if (!filter) return true;
51
- const q = filter.toLowerCase();
52
- return (a.name + ' ' + a.role + ' ' + a.arabic + ' ' + a.type).toLowerCase().includes(q);
53
- });
54
- if (!visible.length) return null;
48
+ // Session-local prompt cache: file name -> raw markdown. Re-opening a card
49
+ // renders from here instead of refetching.
50
+ const promptCache = new Map();
51
+
52
+ // ---- Section: sticky header + card grid ----
53
+ function AgentSection({ section, agents, metaByFile, onOpen }) {
54
+ if (!agents.length) return null;
55
55
  return html`
56
- <div class="memory-group-header">${label} (${visible.length})</div>
57
- <div class="agent-list">
58
- ${visible.map(a => html`<${AgentCard} key=${a.name} agent=${a} />`)}
56
+ <div class="agent-section">
57
+ <div class="agent-section-head">
58
+ ${section.label}
59
+ <span class="agent-section-count">${agents.length}</span>
60
+ </div>
61
+ <div class="agent-grid">
62
+ ${agents.map(a => html`
63
+ <${AgentCard}
64
+ key=${a.name}
65
+ agent=${a}
66
+ meta=${a.file ? metaByFile[a.file] : null}
67
+ onOpen=${onOpen}
68
+ />
69
+ `)}
70
+ </div>
59
71
  </div>
60
72
  `;
61
73
  }
62
74
 
63
75
  // ---- Root AgentsView ----
64
76
  export function AgentsView() {
65
- const [filter, setFilter] = useState('');
77
+ const [filter, setFilter] = useState('');
78
+ const [metaByFile, setMetaByFile] = useState({});
79
+ const [selected, setSelected] = useState(null);
80
+ const [prompt, setPrompt] = useState({ loading: false, error: null, text: null });
81
+
82
+ // One-shot roster metadata fetch — frontmatter summaries only, no bodies.
83
+ useEffect(() => {
84
+ fetch('/api/agents')
85
+ .then(r => r.json())
86
+ .then(list => {
87
+ const map = {};
88
+ for (const a of (Array.isArray(list) ? list : [])) map[a.file] = a;
89
+ setMetaByFile(map);
90
+ })
91
+ .catch(() => { /* chips/summaries are progressive enhancement — cards still render */ });
92
+ }, []);
93
+
94
+ const openAgent = useCallback(async (agent) => {
95
+ setSelected(agent);
96
+ if (!agent.file) {
97
+ setPrompt({ loading: false, error: null, text: null });
98
+ return;
99
+ }
100
+ if (promptCache.has(agent.file)) {
101
+ setPrompt({ loading: false, error: null, text: promptCache.get(agent.file) });
102
+ return;
103
+ }
104
+ setPrompt({ loading: true, error: null, text: null });
105
+ try {
106
+ const resp = await fetch('/api/file?path=' + encodeURIComponent('rcode/agents/' + agent.file));
107
+ if (!resp.ok) {
108
+ const msg = resp.status === 404
109
+ ? 'Prompt file not found: rcode/agents/' + agent.file
110
+ : 'Failed to load prompt (HTTP ' + resp.status + ').';
111
+ setPrompt({ loading: false, error: msg, text: null });
112
+ return;
113
+ }
114
+ const text = await resp.text();
115
+ promptCache.set(agent.file, text);
116
+ setPrompt({ loading: false, error: null, text });
117
+ } catch {
118
+ setPrompt({ loading: false, error: 'Network error.', text: null });
119
+ }
120
+ }, []);
121
+
122
+ const closeDrawer = useCallback(() => setSelected(null), []);
123
+
124
+ // Escape closes the drawer while it is open.
125
+ useEffect(() => {
126
+ if (!selected) return;
127
+ const onKey = (e) => { if (e.key === 'Escape') closeDrawer(); };
128
+ document.addEventListener('keydown', onKey);
129
+ return () => document.removeEventListener('keydown', onKey);
130
+ }, [selected, closeDrawer]);
131
+
132
+ const sections = useMemo(() =>
133
+ SECTIONS.map(s => ({
134
+ section: s,
135
+ agents: agentsForSection(s).filter(a =>
136
+ matchesFilter(a, a.file ? metaByFile[a.file] : null, filter)),
137
+ })),
138
+ [filter, metaByFile]);
139
+
140
+ const visibleCount = sections.reduce((n, s) => n + s.agents.length, 0);
66
141
 
67
142
  return html`
68
143
  <div class="view active" id="view-agents">
69
144
  <div class="view-title">Team</div>
70
- <div class="filter-bar">
145
+ <div class="filter-bar agent-filter-bar">
71
146
  <input
72
147
  class="filter-input"
73
148
  type="text"
74
- placeholder="Filter agents…"
149
+ placeholder="Search agents by name, role, model, or tool…"
75
150
  value=${filter}
76
151
  onInput=${e => setFilter(e.target.value)}
77
152
  />
153
+ <span class="agent-count">${visibleCount} agent${visibleCount === 1 ? '' : 's'}</span>
78
154
  </div>
79
- <${AgentGroup} label="Team" agents=${REAL_AGENTS} filter=${filter} />
80
- <${AgentGroup} label="AI Agents" agents=${AI_AGENTS} filter=${filter} />
155
+ ${visibleCount === 0
156
+ ? html`<div class="empty">No agents match “${filter}”.</div>`
157
+ : sections.map(({ section, agents }) => html`
158
+ <${AgentSection}
159
+ key=${section.label}
160
+ section=${section}
161
+ agents=${agents}
162
+ metaByFile=${metaByFile}
163
+ onOpen=${openAgent}
164
+ />
165
+ `)}
166
+ ${selected ? html`
167
+ <${AgentDrawer}
168
+ agent=${selected}
169
+ meta=${selected.file ? metaByFile[selected.file] : null}
170
+ prompt=${prompt}
171
+ onClose=${closeDrawer}
172
+ />
173
+ ` : null}
81
174
  </div>
82
175
  `;
83
176
  }
@@ -2,46 +2,17 @@
2
2
  * FilesView — Preact port of the Files view from client-main.js.
3
3
  *
4
4
  * On mount: fetches /api/files to build the grouped file tree.
5
- * Clicking a file: fetches /api/file?path=... and renders markdown via the
6
- * global `marked` CDN lib (stays a CDN global unchanged from legacy).
5
+ * Clicking a file: opens the shared FileReader slide-over, which fetches
6
+ * /api/file?path=... and renders markdown via the global `marked` CDN lib.
7
7
  *
8
8
  * Agent-jump bridge: when the store field `requestedFile` is set (by
9
9
  * AgentsView), FilesView picks it up and pre-fills the search filter, then
10
10
  * clears the field so subsequent renders don't re-trigger.
11
11
  */
12
12
 
13
- import { html, useState, useEffect, useCallback } from '../preact.js';
13
+ import { html, useState, useEffect } from '../preact.js';
14
14
  import { useStore, setState } from '../store.js';
15
- import { showToast } from '../components/shared.js';
16
-
17
- // ---- Markdown helpers (ported from client-main.js:287-294) ----
18
- function stripFrontmatter(md) {
19
- if (!md.startsWith('---')) return md;
20
- const end = md.indexOf('\n---', 3);
21
- return end === -1 ? md : md.slice(end + 4).trimStart();
22
- }
23
-
24
- // Minimal HTML sanitizer for rendered markdown. No DOMPurify dependency on the
25
- // client, so we strip the dangerous primitives via regex after marked emits
26
- // HTML: script/iframe/object/embed tags, inline event handlers, and
27
- // javascript:/data: URLs in href/src. Markdown content comes from the project
28
- // dir (semi-trusted) but may include attacker-controlled text checked into a
29
- // repo, so we cannot trust raw HTML passthrough.
30
- function sanitizeHtml(html) {
31
- return String(html)
32
- .replace(/<\s*(script|iframe|object|embed|link|meta|style)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, '')
33
- .replace(/<\s*(script|iframe|object|embed|link|meta|style)\b[^>]*\/?>/gi, '')
34
- .replace(/\son[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
35
- .replace(/(href|src|xlink:href)\s*=\s*(["'])\s*(?:javascript|data|vbscript):[^"']*\2/gi, '$1=$2#blocked$2');
36
- }
37
-
38
- function renderMd(md) {
39
- const clean = stripFrontmatter(md);
40
- if (typeof marked === 'undefined') {
41
- return '<pre>' + clean.replace(/</g, '&lt;') + '</pre>';
42
- }
43
- return sanitizeHtml(marked.parse(clean));
44
- }
15
+ import { FileReader } from '../components/FileReader.js';
45
16
 
46
17
  // ---- File tree components ----
47
18
  function FileEntry({ file, extraText, onSelect, isSelected }) {
@@ -117,51 +88,14 @@ function FileGroup({ group, onSelect, selectedPath, filter }) {
117
88
  return null;
118
89
  }
119
90
 
120
- // ---- File content pane ----
121
- function FileContent({ path, html: htmlContent, loading, error }) {
122
- if (loading) {
123
- return html`
124
- <div id="file-view">
125
- <div class="skeleton"></div>
126
- <div class="skeleton" style="height:200px;"></div>
127
- </div>
128
- `;
129
- }
130
- if (error) {
131
- return html`
132
- <div id="file-view">
133
- <div style="color:var(--accent-red);padding:16px;">${error}</div>
134
- </div>
135
- `;
136
- }
137
- if (!path || !htmlContent) return html`<div id="file-view"></div>`;
138
-
139
- function copyPath() {
140
- navigator.clipboard.writeText(path).then(() => {
141
- showToast('Path copied!');
142
- }).catch(() => {});
143
- }
144
-
145
- return html`
146
- <div id="file-view">
147
- <div class="file-path-header">
148
- <span>${path}</span>
149
- <button class="copy-btn" onClick=${copyPath}>Copy</button>
150
- </div>
151
- <div class="md-render" dangerouslySetInnerHTML=${{ __html: htmlContent }} />
152
- </div>
153
- `;
154
- }
155
-
156
91
  // ---- Root FilesView ----
157
92
  export function FilesView() {
158
93
  const { requestedFile } = useStore();
159
94
 
160
- const [groups, setGroups] = useState([]);
161
- const [loading, setLoading] = useState(true);
162
- const [filter, setFilter] = useState('');
163
- const [selectedPath, setSelectedPath] = useState(null);
164
- const [fileContent, setFileContent] = useState({ html: null, loading: false, error: null });
95
+ const [groups, setGroups] = useState([]);
96
+ const [loading, setLoading] = useState(true);
97
+ const [filter, setFilter] = useState('');
98
+ const [selected, setSelected] = useState(null); // { path, label } | null
165
99
 
166
100
  // Fetch file tree on mount
167
101
  useEffect(() => {
@@ -173,33 +107,15 @@ export function FilesView() {
173
107
  .finally(() => setLoading(false));
174
108
  }, []);
175
109
 
176
- // Agent-jump bridge: requestedFile set by AgentsView
110
+ // Agent-jump bridge: the agent drawer's "View file in Files" sets
111
+ // requestedFile to a project-relative path — open it in the reader.
177
112
  useEffect(() => {
178
113
  if (!requestedFile) return;
179
- setFilter(requestedFile);
114
+ setSelected({ path: requestedFile, label: requestedFile.split('/').pop() });
180
115
  // Clear the bridge field so this doesn't re-trigger
181
116
  setState({ requestedFile: null });
182
117
  }, [requestedFile]);
183
118
 
184
- const loadFile = useCallback(async (file) => {
185
- setSelectedPath(file.path);
186
- setFileContent({ html: null, loading: true, error: null });
187
- try {
188
- const resp = await fetch('/api/file?path=' + encodeURIComponent(file.path));
189
- if (!resp.ok) {
190
- const msg = resp.status === 404
191
- ? 'File not found: ' + file.path
192
- : 'Failed to load file (HTTP ' + resp.status + ').';
193
- setFileContent({ html: null, loading: false, error: msg });
194
- return;
195
- }
196
- const text = await resp.text();
197
- setFileContent({ html: renderMd(text), loading: false, error: null });
198
- } catch {
199
- setFileContent({ html: null, loading: false, error: 'Network error.' });
200
- }
201
- }, []);
202
-
203
119
  return html`
204
120
  <div class="view active" id="view-files">
205
121
  <div class="view-title">Files</div>
@@ -222,20 +138,21 @@ export function FilesView() {
222
138
  <${FileGroup}
223
139
  key=${g.group}
224
140
  group=${g}
225
- onSelect=${loadFile}
226
- selectedPath=${selectedPath}
141
+ onSelect=${setSelected}
142
+ selectedPath=${selected && selected.path}
227
143
  filter=${filter}
228
144
  />
229
145
  `)
230
146
  }
231
147
  </div>
232
148
  </div>
233
- <${FileContent}
234
- path=${selectedPath}
235
- html=${fileContent.html}
236
- loading=${fileContent.loading}
237
- error=${fileContent.error}
238
- />
149
+ ${selected && html`
150
+ <${FileReader}
151
+ path=${selected.path}
152
+ title=${selected.label}
153
+ onClose=${() => setSelected(null)}
154
+ />
155
+ `}
239
156
  </div>
240
157
  `;
241
158
  }
@@ -9,8 +9,9 @@
9
9
 
10
10
  import { html, useState, useCallback } from '../preact.js';
11
11
  import { useStore, refresh } from '../store.js';
12
- import { allTasks } from '../util.js';
13
- import { runStory, stopStory, openOrchPanel } from '../orchestrator.js';
12
+ import { allTasks, currentPhaseName } from '../util.js';
13
+ import { stopStory, openOrchPanel } from '../orchestrator.js';
14
+ import { openRunnerPicker } from '../components/RunnerPicker.js';
14
15
  import { showToast } from '../components/shared.js';
15
16
 
16
17
  // ---- Column descriptors ----
@@ -30,26 +31,26 @@ function kanbanCol(status) {
30
31
  }
31
32
 
32
33
  /** Return the effective column, hoisting to in_progress if a live session exists. */
33
- function effCol(task, activeSessions) {
34
- const running = Array.isArray(activeSessions)
35
- ? activeSessions.some(s => s.storyId === task.id && s.status === 'running')
36
- : false;
37
- return (task.id && running) ? 'in_progress' : kanbanCol(task.status);
34
+ function effCol(task, runningByStory) {
35
+ const running = !!(task.id && runningByStory && runningByStory[task.id]);
36
+ return running ? 'in_progress' : kanbanCol(task.status);
38
37
  }
39
38
 
40
39
  // ---- Card component ----
41
- function KanbanCard({ task, col, onDragStart, onDragEnd }) {
40
+ function KanbanCard({ task, col, live, orchDown, onDragStart, onDragEnd }) {
42
41
  const sid = task.id || '';
43
42
  const c = col;
44
- const isRunning = c === 'in_progress';
45
- const canRun = c === 'todo' || c === 'blocked';
43
+ const isRunning = !!live; // live orchestrator session, not just status
44
+ const canRun = !isRunning && (c === 'todo' || c === 'blocked');
46
45
  const pts = task.points ? task.points + 'p' : null;
47
46
  const phase = task.phaseId ? 'P' + task.phaseId : null;
48
47
  const sprintMeta = [pts, phase].filter(Boolean).join(' · ');
49
48
 
50
49
  function handleRun(e) {
51
50
  e.stopPropagation();
52
- runStory(sid);
51
+ openRunnerPicker(e.currentTarget, {
52
+ kind: 'session', storyId: sid, cmd: '/rcode-dev-story ' + sid, title: sid,
53
+ });
53
54
  }
54
55
  function handleStop(e) {
55
56
  e.stopPropagation();
@@ -80,13 +81,15 @@ function KanbanCard({ task, col, onDragStart, onDragEnd }) {
80
81
  ` : null}
81
82
  ${isRunning ? html`
82
83
  <div class="card-run-indicator" id=${'run-ind-' + sid}>
83
- <span class="run-pulse"></span>running
84
+ <span class="live-dot"></span>running
84
85
  </div>
85
86
  ` : null}
86
87
  ${sid ? html`
87
88
  <div class="kanban-card-actions">
88
89
  ${canRun ? html`
89
- <button class="kanban-run-btn" onClick=${handleRun}>▶ Run</button>
90
+ <button class="kanban-run-btn" disabled=${orchDown}
91
+ title=${orchDown ? 'Orchestrator unreachable' : 'Run ' + sid}
92
+ onClick=${handleRun}>▶ Run</button>
90
93
  ` : isRunning ? html`
91
94
  <button class="kanban-stop-btn" onClick=${handleStop}>■ Stop</button>
92
95
  <button class="kanban-view-btn" onClick=${handleView}>↗ View</button>
@@ -99,8 +102,19 @@ function KanbanCard({ task, col, onDragStart, onDragEnd }) {
99
102
  `;
100
103
  }
101
104
 
105
+ /**
106
+ * Orchestrator status dot — reflects the 4s session-poll reachability.
107
+ * up (green pulse) / down (red) / neutral until the first poll lands.
108
+ */
109
+ function OrchDot({ online }) {
110
+ const cls = 'orch-status-dot' + (online === false ? ' down' : online ? ' up' : '');
111
+ const label = online === false ? 'Orchestrator unreachable'
112
+ : online ? 'Orchestrator online' : 'Orchestrator status unknown';
113
+ return html`<span class=${cls} id="orch-dot" title=${label} role="img" aria-label=${label}></span>`;
114
+ }
115
+
102
116
  // ---- Column component ----
103
- function KanbanColumn({ col, cards, onDragStart, onDragEnd, onDragOver, onDrop }) {
117
+ function KanbanColumn({ col, cards, runningByStory, orchDown, onDragStart, onDragEnd, onDragOver, onDrop }) {
104
118
  return html`
105
119
  <div class=${'kanban-col ' + col.cssClass} data-col=${col.id}>
106
120
  <div class="kanban-col-head">
@@ -120,6 +134,8 @@ function KanbanColumn({ col, cards, onDragStart, onDragEnd, onDragOver, onDrop }
120
134
  key=${t.id || t.title}
121
135
  task=${t}
122
136
  col=${col.id}
137
+ live=${!!(t.id && runningByStory && runningByStory[t.id])}
138
+ orchDown=${orchDown}
123
139
  onDragStart=${e => onDragStart(e, t)}
124
140
  onDragEnd=${onDragEnd}
125
141
  />
@@ -131,8 +147,9 @@ function KanbanColumn({ col, cards, onDragStart, onDragEnd, onDragOver, onDrop }
131
147
 
132
148
  // ---- Root KanbanView ----
133
149
  export function KanbanView() {
134
- const { phases, activeSessions, currentPhase, milestone } = useStore();
150
+ const { phases, runningByStory, currentPhase, milestone, orchOnline } = useStore();
135
151
  const tasks = allTasks(phases);
152
+ const orchDown = orchOnline === false;
136
153
 
137
154
  // ---- Local column state (visual DnD overrides) ----
138
155
  // Map<taskId, colId> — overrides the store-derived column for visual-only moves.
@@ -141,7 +158,7 @@ export function KanbanView() {
141
158
 
142
159
  function getColFor(task) {
143
160
  if (visualMoves[task.id]) return visualMoves[task.id];
144
- return effCol(task, activeSessions);
161
+ return effCol(task, runningByStory);
145
162
  }
146
163
 
147
164
  // Build buckets
@@ -190,7 +207,7 @@ export function KanbanView() {
190
207
  <div class="view active" id="view-kanban">
191
208
  <div class="kanban-topbar">
192
209
  <div class="kanban-topbar-title">
193
- <span class="orch-status-dot" id="orch-dot"></span>
210
+ <${OrchDot} online=${orchOnline} />
194
211
  Kanban
195
212
  </div>
196
213
  <div class="kanban-topbar-actions">
@@ -200,11 +217,11 @@ export function KanbanView() {
200
217
  </div>
201
218
  <div class="empty" style="margin:24px;">
202
219
  No stories yet.
203
- ${(milestone || currentPhase) ? html`
220
+ ${(milestone || currentPhaseName(currentPhase)) ? html`
204
221
  <div class="empty-action">
205
222
  ${milestone ? html`Milestone <strong>${milestone}</strong>` : null}
206
- ${milestone && currentPhase ? ' · ' : null}
207
- ${currentPhase ? html`Phase <strong>${currentPhase}</strong>` : null}
223
+ ${milestone && currentPhaseName(currentPhase) ? ' · ' : null}
224
+ ${currentPhaseName(currentPhase) ? html`Phase <strong>${currentPhaseName(currentPhase)}</strong>` : null}
208
225
  ${' is active.'}
209
226
  </div>
210
227
  ` : null}
@@ -221,7 +238,7 @@ export function KanbanView() {
221
238
  <div class="view active" id="view-kanban">
222
239
  <div class="kanban-topbar">
223
240
  <div class="kanban-topbar-title">
224
- <span class="orch-status-dot" id="orch-dot"></span>
241
+ <${OrchDot} online=${orchOnline} />
225
242
  Kanban
226
243
  </div>
227
244
  <div class="kanban-topbar-actions">
@@ -235,6 +252,8 @@ export function KanbanView() {
235
252
  key=${col.id}
236
253
  col=${col}
237
254
  cards=${buckets[col.id] || []}
255
+ runningByStory=${runningByStory}
256
+ orchDown=${orchDown}
238
257
  onDragStart=${handleDragStart}
239
258
  onDragEnd=${handleDragEnd}
240
259
  onDragOver=${handleDragOver}
@@ -7,9 +7,13 @@
7
7
  * populated — sections map + distillates / change records / archive / post-mortems
8
8
  *
9
9
  * Command hints accordion mirrors the legacy cmdAccordion() output.
10
+ *
11
+ * Clicking an existing entry opens its content in the shared FileReader
12
+ * slide-over (same component the Files view uses).
10
13
  */
11
14
 
12
15
  import { html, useState, useEffect } from '../preact.js';
16
+ import { FileReader } from '../components/FileReader.js';
13
17
 
14
18
  // ---- Command hints accordion ----
15
19
  const MEMORY_HINTS = [
@@ -39,7 +43,7 @@ function CmdAccordion({ hints }) {
39
43
  }
40
44
 
41
45
  // ---- Section file list ----
42
- function SectionGroup({ section, files }) {
46
+ function SectionGroup({ section, files, onOpen }) {
43
47
  return html`
44
48
  <div>
45
49
  <div class="memory-group-header">${section}</div>
@@ -47,8 +51,13 @@ function SectionGroup({ section, files }) {
47
51
  ${files.map(f => {
48
52
  const status = f.exists ? (f.populated ? '✓' : '○') : '✗';
49
53
  const meta = f.exists ? (f.populated ? 'populated' : 'template only') : 'missing';
54
+ // Only existing files are openable — missing entries stay inert.
50
55
  return html`
51
- <div class="item" key=${f.name}>
56
+ <div
57
+ class=${'item' + (f.exists ? ' item-clickable' : '')}
58
+ key=${f.name}
59
+ onClick=${f.exists ? () => onOpen(f) : undefined}
60
+ >
52
61
  <div class="item-title">${status} ${f.name}</div>
53
62
  <div class="item-meta">${meta} · ${f.bytes || 0} bytes</div>
54
63
  </div>
@@ -60,14 +69,14 @@ function SectionGroup({ section, files }) {
60
69
  }
61
70
 
62
71
  // ---- Generic list group (distillates, change records, etc.) ----
63
- function ListGroup({ label, items }) {
72
+ function ListGroup({ label, items, onOpen }) {
64
73
  if (!items || !items.length) return null;
65
74
  return html`
66
75
  <div>
67
76
  <div class="memory-group-header">${label} (${items.length})</div>
68
77
  <div class="decision-list">
69
78
  ${items.map(f => html`
70
- <div class="item" key=${f.name}>
79
+ <div class="item item-clickable" key=${f.name} onClick=${() => onOpen(f)}>
71
80
  <div class="item-title">${f.name}</div>
72
81
  </div>
73
82
  `)}
@@ -81,6 +90,7 @@ export function MemoryView() {
81
90
  const [memory, setMemory] = useState(null);
82
91
  const [loading, setLoading] = useState(true);
83
92
  const [error, setError] = useState(null);
93
+ const [reader, setReader] = useState(null); // { path, name } | null
84
94
 
85
95
  useEffect(() => {
86
96
  setLoading(true);
@@ -144,14 +154,21 @@ export function MemoryView() {
144
154
  </div>
145
155
  <div id="memory-sections">
146
156
  ${Object.entries(sections).map(([section, files]) => html`
147
- <${SectionGroup} key=${section} section=${section} files=${files} />
157
+ <${SectionGroup} key=${section} section=${section} files=${files} onOpen=${setReader} />
148
158
  `)}
149
- <${ListGroup} label="Distillates" items=${memory.distillates} />
150
- <${ListGroup} label="Change Records" items=${memory.changeRecords} />
151
- <${ListGroup} label="Milestone Archive" items=${memory.archive} />
152
- <${ListGroup} label="Post-mortems" items=${memory.postMortems} />
159
+ <${ListGroup} label="Distillates" items=${memory.distillates} onOpen=${setReader} />
160
+ <${ListGroup} label="Change Records" items=${memory.changeRecords} onOpen=${setReader} />
161
+ <${ListGroup} label="Milestone Archive" items=${memory.archive} onOpen=${setReader} />
162
+ <${ListGroup} label="Post-mortems" items=${memory.postMortems} onOpen=${setReader} />
153
163
  </div>
154
164
  <${CmdAccordion} hints=${MEMORY_HINTS} />
165
+ ${reader && html`
166
+ <${FileReader}
167
+ path=${reader.path}
168
+ title=${reader.name}
169
+ onClose=${() => setReader(null)}
170
+ />
171
+ `}
155
172
  </div>
156
173
  `;
157
174
  }