@hanzlaa/rcode 4.1.2 → 4.3.1
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/cli/install.js +176 -13
- package/cli/lib/config.cjs +4 -2
- package/cli/lib/fsutil.cjs +13 -2
- package/cli/lib/homedir.cjs +21 -0
- package/cli/lib/schemas.cjs +6 -1
- package/cli/nuke.js +13 -8
- package/cli/postinstall.js +14 -4
- package/cli/rcode-slash-router.cjs +118 -0
- package/cli/uninstall.js +59 -1
- package/cli/update.js +10 -5
- package/dist/rcode.js +234 -230
- package/package.json +1 -1
- package/rcode/references/auto-init-guard.md +2 -2
- package/rcode/references/output-format.md +5 -5
- package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/step-10-complete.md +1 -1
- package/server/dashboard.js +33 -13
- package/server/lib/api.js +62 -4
- package/server/lib/html/client/agents-data.js +22 -18
- package/server/lib/html/client/app.js +3 -0
- package/server/lib/html/client/components/AgentCard.js +127 -0
- package/server/lib/html/client/components/App.js +104 -39
- package/server/lib/html/client/components/CommandPalette.js +133 -0
- package/server/lib/html/client/components/FileReader.js +116 -0
- package/server/lib/html/client/components/FilterChips.js +94 -0
- package/server/lib/html/client/components/NotifyCenter.js +117 -0
- package/server/lib/html/client/components/OrchPanel.js +80 -52
- package/server/lib/html/client/components/PhaseGraph.js +300 -0
- package/server/lib/html/client/components/RejectDialog.js +78 -0
- package/server/lib/html/client/components/RunnerPicker.js +190 -0
- package/server/lib/html/client/components/Sidebar.js +106 -61
- package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
- package/server/lib/html/client/components/TaskPipeline.js +83 -0
- package/server/lib/html/client/components/Topbar.js +86 -39
- package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
- package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
- package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
- package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
- package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
- package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
- package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
- package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
- package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
- package/server/lib/html/client/components/shared.js +47 -11
- package/server/lib/html/client/filter-state.js +72 -0
- package/server/lib/html/client/icons-client.js +7 -0
- package/server/lib/html/client/notify.js +75 -0
- package/server/lib/html/client/orchestrator.js +168 -41
- package/server/lib/html/client/preact.js +13 -8
- package/server/lib/html/client/store.js +70 -6
- package/server/lib/html/client/util.js +78 -0
- package/server/lib/html/client/vendor/htm.js +1 -0
- package/server/lib/html/client/vendor/preact-hooks.js +2 -0
- package/server/lib/html/client/vendor/preact.js +2 -0
- package/server/lib/html/client/views/AgentsView.js +144 -51
- package/server/lib/html/client/views/FilesView.js +20 -103
- package/server/lib/html/client/views/KanbanView.js +40 -21
- package/server/lib/html/client/views/MemoryView.js +26 -9
- package/server/lib/html/client/views/MilestonesView.js +4 -4
- package/server/lib/html/client/views/OrchestrationView.js +154 -19
- package/server/lib/html/client/views/OverviewView.js +47 -239
- package/server/lib/html/client/views/PhasesView.js +50 -6
- package/server/lib/html/client/views/RoadmapView.js +6 -3
- package/server/lib/html/client/views/SprintsView.js +50 -6
- package/server/lib/html/client/views/TasksView.js +4 -3
- package/server/lib/html/client.js +21 -4
- package/server/lib/html/css.js +2761 -8
- package/server/lib/html/icons.js +7 -0
- package/server/lib/html/shell.js +10 -3
- package/server/lib/scanner.js +376 -39
- package/server/orchestrator.js +346 -7
|
@@ -1,83 +1,176 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AgentsView —
|
|
2
|
+
* AgentsView — Team roster: category sections of rich agent cards plus an
|
|
3
|
+
* agent detail drawer (components/AgentCard.js).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* that
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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="
|
|
57
|
-
|
|
58
|
-
|
|
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]
|
|
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="
|
|
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
|
-
|
|
80
|
-
|
|
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:
|
|
6
|
-
*
|
|
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
|
|
13
|
+
import { html, useState, useEffect } from '../preact.js';
|
|
14
14
|
import { useStore, setState } from '../store.js';
|
|
15
|
-
import {
|
|
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, '<') + '</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]
|
|
161
|
-
const [loading, setLoading]
|
|
162
|
-
const [filter, setFilter]
|
|
163
|
-
const [
|
|
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:
|
|
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
|
-
|
|
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=${
|
|
226
|
-
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 {
|
|
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,
|
|
34
|
-
const running =
|
|
35
|
-
|
|
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 =
|
|
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
|
-
|
|
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="
|
|
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"
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|