agentopia 1.0.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 (140) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/dist/app.d.ts +10 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +121 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/config.d.ts +9 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +19 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/db/database.d.ts +5 -0
  11. package/dist/db/database.d.ts.map +1 -0
  12. package/dist/db/database.js +39 -0
  13. package/dist/db/database.js.map +1 -0
  14. package/dist/db/schema.d.ts +3 -0
  15. package/dist/db/schema.d.ts.map +1 -0
  16. package/dist/db/schema.js +621 -0
  17. package/dist/db/schema.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +49 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/logger.d.ts +4 -0
  23. package/dist/logger.d.ts.map +1 -0
  24. package/dist/logger.js +9 -0
  25. package/dist/logger.js.map +1 -0
  26. package/dist/middleware/auth.d.ts +13 -0
  27. package/dist/middleware/auth.d.ts.map +1 -0
  28. package/dist/middleware/auth.js +733 -0
  29. package/dist/middleware/auth.js.map +1 -0
  30. package/dist/routes/agents.d.ts +3 -0
  31. package/dist/routes/agents.d.ts.map +1 -0
  32. package/dist/routes/agents.js +1058 -0
  33. package/dist/routes/agents.js.map +1 -0
  34. package/dist/routes/issues.d.ts +4 -0
  35. package/dist/routes/issues.d.ts.map +1 -0
  36. package/dist/routes/issues.js +946 -0
  37. package/dist/routes/issues.js.map +1 -0
  38. package/dist/routes/knowledge.d.ts +3 -0
  39. package/dist/routes/knowledge.d.ts.map +1 -0
  40. package/dist/routes/knowledge.js +117 -0
  41. package/dist/routes/knowledge.js.map +1 -0
  42. package/dist/routes/memories.d.ts +3 -0
  43. package/dist/routes/memories.d.ts.map +1 -0
  44. package/dist/routes/memories.js +115 -0
  45. package/dist/routes/memories.js.map +1 -0
  46. package/dist/routes/messages.d.ts +3 -0
  47. package/dist/routes/messages.d.ts.map +1 -0
  48. package/dist/routes/messages.js +130 -0
  49. package/dist/routes/messages.js.map +1 -0
  50. package/dist/routes/projects.d.ts +3 -0
  51. package/dist/routes/projects.d.ts.map +1 -0
  52. package/dist/routes/projects.js +754 -0
  53. package/dist/routes/projects.js.map +1 -0
  54. package/dist/routes/templates.d.ts +3 -0
  55. package/dist/routes/templates.d.ts.map +1 -0
  56. package/dist/routes/templates.js +117 -0
  57. package/dist/routes/templates.js.map +1 -0
  58. package/dist/routes/ui.d.ts +3 -0
  59. package/dist/routes/ui.d.ts.map +1 -0
  60. package/dist/routes/ui.js +38 -0
  61. package/dist/routes/ui.js.map +1 -0
  62. package/dist/services/agent-hierarchy.d.ts +14 -0
  63. package/dist/services/agent-hierarchy.d.ts.map +1 -0
  64. package/dist/services/agent-hierarchy.js +58 -0
  65. package/dist/services/agent-hierarchy.js.map +1 -0
  66. package/dist/services/agent-issue-batch.d.ts +17 -0
  67. package/dist/services/agent-issue-batch.d.ts.map +1 -0
  68. package/dist/services/agent-issue-batch.js +57 -0
  69. package/dist/services/agent-issue-batch.js.map +1 -0
  70. package/dist/services/controller.d.ts +4 -0
  71. package/dist/services/controller.d.ts.map +1 -0
  72. package/dist/services/controller.js +237 -0
  73. package/dist/services/controller.js.map +1 -0
  74. package/dist/services/langgraph-runner.d.ts +33 -0
  75. package/dist/services/langgraph-runner.d.ts.map +1 -0
  76. package/dist/services/langgraph-runner.js +478 -0
  77. package/dist/services/langgraph-runner.js.map +1 -0
  78. package/dist/services/orchestrator.d.ts +9 -0
  79. package/dist/services/orchestrator.d.ts.map +1 -0
  80. package/dist/services/orchestrator.js +116 -0
  81. package/dist/services/orchestrator.js.map +1 -0
  82. package/dist/services/pre-controller.d.ts +7 -0
  83. package/dist/services/pre-controller.d.ts.map +1 -0
  84. package/dist/services/pre-controller.js +101 -0
  85. package/dist/services/pre-controller.js.map +1 -0
  86. package/dist/services/process-manager.d.ts +67 -0
  87. package/dist/services/process-manager.d.ts.map +1 -0
  88. package/dist/services/process-manager.js +938 -0
  89. package/dist/services/process-manager.js.map +1 -0
  90. package/dist/services/project-permissions.d.ts +84 -0
  91. package/dist/services/project-permissions.d.ts.map +1 -0
  92. package/dist/services/project-permissions.js +129 -0
  93. package/dist/services/project-permissions.js.map +1 -0
  94. package/dist/services/scheduler.d.ts +6 -0
  95. package/dist/services/scheduler.d.ts.map +1 -0
  96. package/dist/services/scheduler.js +300 -0
  97. package/dist/services/scheduler.js.map +1 -0
  98. package/dist/services/system-prompt.d.ts +3 -0
  99. package/dist/services/system-prompt.d.ts.map +1 -0
  100. package/dist/services/system-prompt.js +285 -0
  101. package/dist/services/system-prompt.js.map +1 -0
  102. package/dist/services/terminal.d.ts +18 -0
  103. package/dist/services/terminal.d.ts.map +1 -0
  104. package/dist/services/terminal.js +222 -0
  105. package/dist/services/terminal.js.map +1 -0
  106. package/dist/services/websocket.d.ts +15 -0
  107. package/dist/services/websocket.d.ts.map +1 -0
  108. package/dist/services/websocket.js +204 -0
  109. package/dist/services/websocket.js.map +1 -0
  110. package/dist/types.d.ts +108 -0
  111. package/dist/types.d.ts.map +1 -0
  112. package/dist/types.js +3 -0
  113. package/dist/types.js.map +1 -0
  114. package/env.ini +18 -0
  115. package/package.json +38 -0
  116. package/project_id +0 -0
  117. package/public/admin-users.html +188 -0
  118. package/public/agent.html +199 -0
  119. package/public/css/issues.css +275 -0
  120. package/public/css/style.css +1299 -0
  121. package/public/index.html +166 -0
  122. package/public/issue.html +76 -0
  123. package/public/js/agent.js +19 -0
  124. package/public/js/common.js +735 -0
  125. package/public/js/dashboard.js +772 -0
  126. package/public/js/files-panel.js +703 -0
  127. package/public/js/interactive-terminal.js +201 -0
  128. package/public/js/issue-renderer.js +559 -0
  129. package/public/js/issue.js +57 -0
  130. package/public/js/project.js +2425 -0
  131. package/public/js/terminal.js +564 -0
  132. package/public/project.html +430 -0
  133. package/public/terminal.html +67 -0
  134. package/public/vendor/marked.js +74 -0
  135. package/public/vendor/xterm-addon-fit.js +2 -0
  136. package/public/vendor/xterm.css +209 -0
  137. package/public/vendor/xterm.js +2 -0
  138. package/send_message_and_update_issue.js +65 -0
  139. package/tsconfig.json +19 -0
  140. package/update_round2_and_create_round3.js +284 -0
@@ -0,0 +1,559 @@
1
+ // ─── Shared Issue Renderer ───
2
+ // Used by both issue.js (full page) and dashboard.js (floating panel)
3
+
4
+ var IssueRenderer = (function() {
5
+ var EMOJIS = ['👍','👎','❤️','🎉','😕','🚀'];
6
+
7
+ // Current rendering context
8
+ var _ctx = {
9
+ issue: null,
10
+ agents: [],
11
+ container: null,
12
+ reload: null,
13
+ onAfterAction: null,
14
+ };
15
+
16
+ function nameOf(id) {
17
+ if (id === 'user') return 'User';
18
+ if (id === 'all') return 'All';
19
+ var a = _ctx.agents.find(function(x) { return x.id === id; });
20
+ if (a) return a.name;
21
+ return (id || '').slice(0, 8);
22
+ }
23
+
24
+ function renderMd(text) {
25
+ if (!text) return '';
26
+ var agents = _ctx.agents;
27
+ var issue = _ctx.issue;
28
+
29
+ // Protect LaTeX blocks from markdown processing
30
+ var latexBlocks = [];
31
+ var processed = text;
32
+ // Block math: $$...$$
33
+ processed = processed.replace(/\$\$([\s\S]+?)\$\$/g, function(_, tex) {
34
+ var idx = latexBlocks.length;
35
+ latexBlocks.push({ tex: tex.trim(), display: true });
36
+ return '%%LATEX_BLOCK_' + idx + '%%';
37
+ });
38
+ // Inline math: $...$
39
+ processed = processed.replace(/(?<!\$)\$(?!\$)([^\n$]+?)\$(?!\$)/g, function(_, tex) {
40
+ var idx = latexBlocks.length;
41
+ latexBlocks.push({ tex: tex.trim(), display: false });
42
+ return '%%LATEX_BLOCK_' + idx + '%%';
43
+ });
44
+ // Auto-link #N to issue pages
45
+ processed = processed.replace(/#(\d+)/g, function(m, n) {
46
+ return issue && issue.project_id ? '[#' + n + '](/projects/' + issue.project_id + '/issues/' + n + ')' : m;
47
+ });
48
+
49
+ var html = '';
50
+ if (typeof marked !== 'undefined') {
51
+ try { html = marked.parse(processed); } catch(e) { html = '<pre style="white-space:pre-wrap">' + esc(text) + '</pre>'; }
52
+ } else {
53
+ html = '<pre style="white-space:pre-wrap">' + esc(text) + '</pre>';
54
+ }
55
+
56
+ // Highlight @mentions
57
+ var agentNames = agents.map(function(a) { return a.name; });
58
+ html = html.replace(/@([\w-]+)/g, function(m, name) {
59
+ var isAgent = agentNames.includes(name);
60
+ return '<span style="color:' + (isAgent ? '#61afef' : '#e5c07b') + ';font-weight:500;background:' + (isAgent ? '#61afef18' : '#e5c07b18') + ';padding:0 4px;border-radius:3px">' + m + '</span>';
61
+ });
62
+
63
+ // Restore LaTeX blocks with KaTeX rendering
64
+ html = html.replace(/%%LATEX_BLOCK_(\d+)%%/g, function(_, idx) {
65
+ var block = latexBlocks[parseInt(idx)];
66
+ if (typeof katex !== 'undefined') {
67
+ try { return katex.renderToString(block.tex, { displayMode: block.display, throwOnError: false }); }
68
+ catch(e) { return '<code style="color:var(--error)">' + block.tex + '</code>'; }
69
+ }
70
+ return '<code>' + esc(block.tex) + '</code>';
71
+ });
72
+
73
+ return html;
74
+ }
75
+
76
+ function labelHtml(text) {
77
+ var colors = ['#e06c75','#98c379','#e5c07b','#61afef','#c678dd','#56b6c2','#d19a66','#b5bd68','#cc6666','#8abeb7'];
78
+ var bg = colors[hashCode(text.trim()) % colors.length];
79
+ return '<span style="font-size:11px;padding:1px 8px;border-radius:12px;background:' + bg + '22;color:' + bg + ';border:1px solid ' + bg + '44;font-weight:500">' + esc(text.trim()) + '</span>';
80
+ }
81
+
82
+ function statusIcon(s) {
83
+ if (s === 'open') return '<svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="none" stroke="#3fb950" stroke-width="2"/><circle cx="8" cy="8" r="2" fill="#3fb950"/></svg>';
84
+ if (s === 'in_progress') return '<svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="none" stroke="#d29922" stroke-width="2"/><circle cx="8" cy="8" r="2" fill="#d29922"/></svg>';
85
+ if (s === 'pending') return '<svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="none" stroke="#d29922" stroke-width="2" stroke-dasharray="4 2"/><circle cx="8" cy="8" r="2" fill="#d29922"/></svg>';
86
+ if (s === 'done') return '<svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="none" stroke="#8b6fcf" stroke-width="2"/><path d="M5.5 8l2 2 3.5-3.5" fill="none" stroke="#8b6fcf" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
87
+ return '<svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="none" stroke="gray" stroke-width="2"/><line x1="5" y1="5" x2="11" y2="11" stroke="gray" stroke-width="1.5"/><line x1="11" y1="5" x2="5" y2="11" stroke="gray" stroke-width="1.5"/></svg>';
88
+ }
89
+
90
+ function reactionBar(targetType, targetId, reactions) {
91
+ var grouped = {};
92
+ (reactions || []).forEach(function(r) { if (!grouped[r.emoji]) grouped[r.emoji] = []; grouped[r.emoji].push(r.user_id); });
93
+ var html = Object.entries(grouped).map(function(entry) {
94
+ var emoji = entry[0], users = entry[1];
95
+ var title = users.map(function(u) { return nameOf(u); }).join(', ');
96
+ return '<button onclick="IssueRenderer.toggleReaction(\'' + targetType + '\',\'' + targetId + '\',\'' + emoji + '\')" style="background:var(--selected-bg);border:1px solid var(--border);border-radius:12px;padding:1px 8px;cursor:pointer;font-size:12px" title="' + title + '">' + emoji + ' ' + users.length + '</button>';
97
+ }).join(' ');
98
+ html += ' <button onclick="IssueRenderer.showEmojiPicker(\'' + targetType + '\',\'' + targetId + '\')" style="background:none;border:1px solid var(--border);border-radius:12px;padding:1px 6px;cursor:pointer;font-size:12px" title="Add reaction">+</button>';
99
+ return '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' + html + '</div>';
100
+ }
101
+
102
+ // ─── Main render function ───
103
+ function render(issue, agents, container, options) {
104
+ options = options || {};
105
+ _ctx.issue = issue;
106
+ _ctx.agents = agents || [];
107
+ _ctx.container = container;
108
+ _ctx.reload = options.reload || function() {};
109
+ _ctx.onAfterAction = options.onAfterAction || function() {};
110
+
111
+ var labels = issue.labels ? issue.labels.split(',').filter(function(l) { return l.trim(); }).map(function(l) { return labelHtml(l); }).join(' ') : '';
112
+ var assignOpts = '<option value="">Unassigned</option><option value="all" ' + ('all'===issue.assigned_to?'selected':'') + '>All</option><option value="user" ' + ('user'===issue.assigned_to?'selected':'') + '>User</option>' +
113
+ agents.map(function(a) { return '<option value="' + a.id + '" ' + (a.id===issue.assigned_to?'selected':'') + '>' + esc(a.name) + '</option>'; }).join('');
114
+
115
+ // Build timeline: events + comments
116
+ var allEntries = issue.comments || [];
117
+ var timeline = allEntries.map(function(c) {
118
+ var entryDate = c.created_at ? new Date(c.created_at + (c.created_at.includes('Z') ? '' : 'Z')).toLocaleString() : '';
119
+ if (c.event_type !== 'comment') {
120
+ var icon = c.event_type === 'status_change' ? '🔄' : c.event_type === 'assignment' ? '👤' : '🏷️';
121
+ return '<div style="display:flex;align-items:center;gap:8px;padding:8px 0 8px 40px;font-size:12px;color:var(--text-secondary)">' +
122
+ '<span>' + icon + '</span>' +
123
+ '<span><strong>' + esc(nameOf(c.author_id)) + '</strong> ' + esc(c.body.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, function(id) { return nameOf(id); })) + ' <span title="' + esc(entryDate) + '" style="cursor:default">' + timeAgo(c.created_at) + '</span></span>' +
124
+ '</div>';
125
+ }
126
+ return '<div class="timeline-item">' +
127
+ '<div class="timeline-avatar" style="background:none;border:none">' + avatarSvg(nameOf(c.author_id), 24) + '</div>' +
128
+ '<div class="timeline-comment">' +
129
+ '<div class="timeline-comment-header" style="display:flex;justify-content:space-between;align-items:center">' +
130
+ '<span><strong>' + esc(nameOf(c.author_id)) + '</strong> commented <span title="' + esc(entryDate) + '" style="cursor:default">' + timeAgo(c.created_at) + '</span></span>' +
131
+ '<span style="display:flex;gap:4px">' +
132
+ (c.author_id === 'user' ? '<button onclick="IssueRenderer.editComment(\'' + c.id + '\')" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:11px">edit</button><button onclick="IssueRenderer.deleteComment(\'' + c.id + '\')" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:11px">delete</button>' : '') +
133
+ '</span>' +
134
+ '</div>' +
135
+ '<div class="timeline-comment-body markdown-body" id="ir-comment-body-' + c.id + '">' + renderMd(c.body) + '</div>' +
136
+ reactionBar('comment', c.id, c.reactions) +
137
+ '</div>' +
138
+ '</div>';
139
+ }).join('');
140
+
141
+ var commentCount = allEntries.filter(function(c) { return c.event_type === 'comment'; }).length;
142
+
143
+ container.innerHTML =
144
+ '<div style="margin-bottom:16px">' +
145
+ '<div style="display:flex;align-items:flex-start;gap:8px" id="ir-title-display">' +
146
+ '<h2 style="flex:1;font-size:22px;font-weight:600">' + esc(issue.title) + ' <span style="color:var(--text-secondary);font-weight:400">#' + issue.number + '</span></h2>' +
147
+ '<button class="btn btn-sm" onclick="IssueRenderer.startEditTitle()">Edit</button>' +
148
+ '<a href="/projects/' + issue.project_id + '/issues/' + issue.number + '" class="btn btn-sm" title="Open in a new page" style="text-decoration:none">↗</a>' +
149
+ '</div>' +
150
+ '<div id="ir-title-edit" style="display:none;margin-bottom:8px">' +
151
+ '<div style="display:flex;gap:8px">' +
152
+ '<input type="text" id="ir-edit-title-input" style="flex:1;padding:6px 10px;font-size:16px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">' +
153
+ '<button class="btn btn-sm btn-primary" onclick="IssueRenderer.saveTitle()">Save</button>' +
154
+ '<button class="btn btn-sm" onclick="IssueRenderer.cancelEditTitle()">Cancel</button>' +
155
+ '</div>' +
156
+ '</div>' +
157
+ '<div style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:13px">' +
158
+ statusIcon(issue.status) +
159
+ '<span style="font-weight:500">' + issue.status.replace('_',' ') + '</span>' +
160
+ priorityBadge(issue.priority) + ' ' + labels +
161
+ '<span style="color:var(--text-secondary)">' + esc(nameOf(issue.created_by)) + ' opened ' + timeAgo(issue.created_at) + ' · ' + commentCount + ' comments</span>' +
162
+ '</div>' +
163
+ '</div>' +
164
+
165
+ '<div class="issue-detail-layout">' +
166
+ '<div class="issue-detail-main">' +
167
+ '<div class="issue-body">' +
168
+ '<div class="issue-body-header" style="display:flex;justify-content:space-between;align-items:center">' +
169
+ '<span style="display:flex;align-items:center;gap:6px">' + avatarSvg(nameOf(issue.created_by), 20) + ' <strong>' + esc(nameOf(issue.created_by)) + '</strong></span>' +
170
+ '<button onclick="IssueRenderer.startEditBody()" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:11px">edit</button>' +
171
+ '</div>' +
172
+ '<div class="issue-body-content" id="ir-body-display">' +
173
+ '<div class="markdown-body">' + renderMd(issue.body) + '</div>' +
174
+ reactionBar('issue', issue.id, issue.reactions) +
175
+ '</div>' +
176
+ '<div id="ir-body-edit" style="display:none;padding:12px">' +
177
+ '<textarea id="ir-edit-body-input" rows="8" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-size:13px;font-family:inherit;resize:vertical"></textarea>' +
178
+ '<div style="display:flex;gap:8px;margin-top:8px;justify-content:flex-end">' +
179
+ '<button class="btn btn-sm" onclick="IssueRenderer.cancelEditBody()">Cancel</button>' +
180
+ '<button class="btn btn-sm btn-primary" onclick="IssueRenderer.saveBody()">Save</button>' +
181
+ '</div>' +
182
+ '</div>' +
183
+ '</div>' +
184
+
185
+ (timeline ? '<div class="timeline">' + timeline + '</div>' : '') +
186
+
187
+ '<div class="comment-box" style="margin-top:16px">' +
188
+ '<textarea id="ir-comment-input" placeholder="Leave a comment... (Markdown supported)"></textarea>' +
189
+ '<div class="comment-box-footer" style="display:flex;justify-content:space-between;align-items:center">' +
190
+ '<span style="font-size:11px;color:var(--text-secondary)">Markdown · #N auto-links · @agent-name to mention</span>' +
191
+ '<div style="display:flex;gap:8px;align-items:center">' +
192
+ (issue.status !== 'closed' && issue.status !== 'done'
193
+ ? '<button class="btn btn-sm" id="ir-close-issue-btn" onclick="IssueRenderer.closeWithComment()" style="color:var(--error);border-color:var(--error)">Close issue</button>'
194
+ : '<button class="btn btn-sm" id="ir-reopen-issue-btn" onclick="IssueRenderer.reopenWithComment()" style="color:var(--success);border-color:var(--success)">Reopen issue</button>') +
195
+ '<button class="btn btn-sm btn-primary" onclick="IssueRenderer.addComment()">Comment</button>' +
196
+ '</div>' +
197
+ '</div>' +
198
+ '</div>' +
199
+ '</div>' +
200
+
201
+ '<div class="issue-detail-sidebar">' +
202
+ '<div class="sidebar-section">' +
203
+ '<div class="sidebar-section-title">Status</div>' +
204
+ '<select id="ir-detail-status" onchange="IssueRenderer.updateField(\'status\',this.value)" style="width:100%;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-size:12px">' +
205
+ '<option value="open" ' + (issue.status==='open'?'selected':'') + '>Open</option>' +
206
+ '<option value="in_progress" ' + (issue.status==='in_progress'?'selected':'') + '>In Progress</option>' +
207
+ '<option value="pending" ' + (issue.status==='pending'?'selected':'') + '>Pending</option>' +
208
+ '<option value="done" ' + (issue.status==='done'?'selected':'') + '>Done</option>' +
209
+ '<option value="closed" ' + (issue.status==='closed'?'selected':'') + '>Closed</option>' +
210
+ '</select>' +
211
+ '</div>' +
212
+ '<div class="sidebar-section">' +
213
+ '<div class="sidebar-section-title">Assignee</div>' +
214
+ '<select id="ir-detail-assign" onchange="IssueRenderer.updateField(\'assigned_to\',this.value||null)" style="width:100%;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-size:12px">' +
215
+ assignOpts +
216
+ '</select>' +
217
+ '</div>' +
218
+ '<div class="sidebar-section">' +
219
+ '<div class="sidebar-section-title">Labels</div>' +
220
+ '<input type="text" id="ir-detail-labels" value="' + esc(issue.labels||'') + '" placeholder="bug, feature" onchange="IssueRenderer.updateField(\'labels\',this.value)" style="width:100%;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-size:12px">' +
221
+ '</div>' +
222
+ '<div class="sidebar-section">' +
223
+ '<div class="sidebar-section-title">Priority</div>' +
224
+ priorityBadge(issue.priority) +
225
+ '</div>' +
226
+ (issue.parent_id && issue.parent_number ?
227
+ '<div class="sidebar-section">' +
228
+ '<div class="sidebar-section-title">Parent Issue</div>' +
229
+ '<a href="/projects/' + issue.project_id + '/issues/' + issue.parent_number + '" style="font-size:12px;text-decoration:none;display:flex;align-items:center;gap:4px">' +
230
+ statusIcon(issue.parent_status || 'open') + ' #' + issue.parent_number + ' ' + esc(issue.parent_title || '') +
231
+ '</a>' +
232
+ '</div>' : '') +
233
+ (issue.children && issue.children.length > 0 ? (function() {
234
+ var done = issue.children.filter(function(c) { return c.status === 'done' || c.status === 'closed'; }).length;
235
+ var total = issue.children.length;
236
+ var pct = Math.round(done / total * 100);
237
+ return '<div class="sidebar-section">' +
238
+ '<div class="sidebar-section-title">Child Issues (' + done + '/' + total + ' done)</div>' +
239
+ '<div style="background:var(--border);border-radius:4px;height:6px;margin-bottom:8px;overflow:hidden">' +
240
+ '<div style="background:var(--success);height:100%;width:' + pct + '%;transition:width 0.3s"></div>' +
241
+ '</div>' +
242
+ issue.children.map(function(c) {
243
+ return '<a href="/projects/' + issue.project_id + '/issues/' + c.number + '" style="display:flex;align-items:center;gap:6px;padding:3px 0;text-decoration:none;color:inherit;font-size:12px">' +
244
+ statusIcon(c.status) + ' <span>#' + c.number + ' ' + esc(c.title) + '</span></a>';
245
+ }).join('') +
246
+ '</div>';
247
+ })() : '') +
248
+ // ─── Dependencies / Relations ───
249
+ (function() {
250
+ var blocks = issue.blocks || [];
251
+ var blocked_by = issue.blocked_by || [];
252
+ var related_to = issue.related_to || [];
253
+ if (blocks.length === 0 && blocked_by.length === 0 && related_to.length === 0 && !issue.is_blocked) {
254
+ // Show add button even if no relations
255
+ return '<div class="sidebar-section">' +
256
+ '<div class="sidebar-section-title">Dependencies</div>' +
257
+ '<div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">No dependencies</div>' +
258
+ '<button class="btn btn-sm" onclick="IssueRenderer.showAddRelation()" style="font-size:11px;width:100%">+ Add dependency</button>' +
259
+ '</div>';
260
+ }
261
+ var html = '<div class="sidebar-section">';
262
+ html += '<div class="sidebar-section-title">Dependencies';
263
+ if (issue.is_blocked) html += ' <span style="color:var(--error);font-size:10px;font-weight:600;background:var(--error)18;padding:1px 6px;border-radius:8px;margin-left:4px">BLOCKED</span>';
264
+ html += '</div>';
265
+ if (blocked_by.length > 0) {
266
+ html += '<div style="font-size:11px;color:var(--text-secondary);margin-bottom:2px;font-weight:600">Blocked by</div>';
267
+ blocked_by.forEach(function(r) {
268
+ var st = r.status || r.source_status || 'open';
269
+ var resolved = (st === 'done' || st === 'closed');
270
+ html += '<div style="display:flex;align-items:center;gap:4px;padding:2px 0;font-size:12px' + (resolved ? ';color:var(--text-secondary)' : '') + '">' +
271
+ statusIcon(st) +
272
+ ' <a href="/projects/' + issue.project_id + '/issues/' + (r.number || r.source_number) + '" style="text-decoration:none;' + (resolved ? 'color:var(--text-secondary);text-decoration:line-through' : 'color:inherit') + ';flex:1">#' + (r.number || r.source_number) + ' ' + esc(r.title || r.source_title || '') + '</a>' +
273
+ (resolved ? '<span style="font-size:10px;color:var(--text-secondary);background:var(--bg-secondary);padding:0 4px;border-radius:4px;white-space:nowrap">Resolved</span>' : '') +
274
+ '<button onclick="IssueRenderer.removeRelation(\'' + r.relation_id + '\')" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:10px" title="Remove">✕</button>' +
275
+ '</div>';
276
+ });
277
+ }
278
+ if (blocks.length > 0) {
279
+ html += '<div style="font-size:11px;color:var(--text-secondary);margin-bottom:2px;margin-top:4px;font-weight:600">Blocks</div>';
280
+ blocks.forEach(function(r) {
281
+ html += '<div style="display:flex;align-items:center;gap:4px;padding:2px 0;font-size:12px">' +
282
+ statusIcon(r.status || r.target_status || 'open') +
283
+ ' <a href="/projects/' + issue.project_id + '/issues/' + (r.number || r.target_number) + '" style="text-decoration:none;color:inherit;flex:1">#' + (r.number || r.target_number) + ' ' + esc(r.title || r.target_title || '') + '</a>' +
284
+ '<button onclick="IssueRenderer.removeRelation(\'' + r.relation_id + '\')" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:10px" title="Remove">✕</button>' +
285
+ '</div>';
286
+ });
287
+ }
288
+ if (related_to.length > 0) {
289
+ html += '<div style="font-size:11px;color:var(--text-secondary);margin-bottom:2px;margin-top:4px;font-weight:600">Related to</div>';
290
+ related_to.forEach(function(r) {
291
+ var num = r.number || r.target_number || r.source_number;
292
+ var title = r.title || r.target_title || r.source_title || '';
293
+ var st = r.status || r.target_status || r.source_status || 'open';
294
+ html += '<div style="display:flex;align-items:center;gap:4px;padding:2px 0;font-size:12px">' +
295
+ statusIcon(st) +
296
+ ' <a href="/projects/' + issue.project_id + '/issues/' + num + '" style="text-decoration:none;color:inherit;flex:1">#' + num + ' ' + esc(title) + '</a>' +
297
+ '<button onclick="IssueRenderer.removeRelation(\'' + r.relation_id + '\')" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:10px" title="Remove">✕</button>' +
298
+ '</div>';
299
+ });
300
+ }
301
+ html += '<button class="btn btn-sm" onclick="IssueRenderer.showAddRelation()" style="font-size:11px;width:100%;margin-top:6px">+ Add dependency</button>';
302
+ html += '</div>';
303
+ return html;
304
+ })() +
305
+
306
+ (issue.status === 'open' ? '<div style="margin-top:12px"><button class="btn btn-sm btn-danger" onclick="IssueRenderer.deleteIssue()">Delete</button></div>' : '') +
307
+ '</div>' +
308
+ '</div>';
309
+
310
+ // Setup @mention autocomplete
311
+ var commentInput = document.getElementById('ir-comment-input');
312
+ if (commentInput && typeof setupMentionAutocomplete === 'function') {
313
+ setupMentionAutocomplete(commentInput, agents);
314
+ commentInput.addEventListener('input', function() {
315
+ var btn = document.getElementById('ir-close-issue-btn');
316
+ if (btn) btn.textContent = this.value.trim() ? 'Close with comment' : 'Close issue';
317
+ var reopenBtn = document.getElementById('ir-reopen-issue-btn');
318
+ if (reopenBtn) reopenBtn.textContent = this.value.trim() ? 'Reopen with comment' : 'Reopen issue';
319
+ });
320
+ }
321
+ }
322
+
323
+ // ─── Inline editing ───
324
+
325
+ function startEditTitle() {
326
+ document.getElementById('ir-title-display').style.display = 'none';
327
+ document.getElementById('ir-title-edit').style.display = '';
328
+ document.getElementById('ir-edit-title-input').value = _ctx.issue.title;
329
+ document.getElementById('ir-edit-title-input').focus();
330
+ }
331
+ function cancelEditTitle() {
332
+ document.getElementById('ir-title-display').style.display = '';
333
+ document.getElementById('ir-title-edit').style.display = 'none';
334
+ }
335
+ function saveTitle() {
336
+ var v = document.getElementById('ir-edit-title-input').value.trim();
337
+ if (!v) return;
338
+ fetch('/api/issues/' + _ctx.issue.id, { method: 'PUT', headers: apiHeaders(), body: JSON.stringify({ title: v, actor: 'user' }) })
339
+ .then(function() { _ctx.reload(); });
340
+ }
341
+ function startEditBody() {
342
+ document.getElementById('ir-body-display').style.display = 'none';
343
+ document.getElementById('ir-body-edit').style.display = '';
344
+ document.getElementById('ir-edit-body-input').value = _ctx.issue.body;
345
+ document.getElementById('ir-edit-body-input').focus();
346
+ }
347
+ function cancelEditBody() {
348
+ document.getElementById('ir-body-display').style.display = '';
349
+ document.getElementById('ir-body-edit').style.display = 'none';
350
+ }
351
+ function saveBody() {
352
+ var v = document.getElementById('ir-edit-body-input').value;
353
+ fetch('/api/issues/' + _ctx.issue.id, { method: 'PUT', headers: apiHeaders(), body: JSON.stringify({ body: v, actor: 'user' }) })
354
+ .then(function() { _ctx.reload(); });
355
+ }
356
+
357
+ // ─── Actions ───
358
+
359
+ function updateField(field, value) {
360
+ var body = {}; body[field] = value; body.actor = 'user';
361
+ fetch('/api/issues/' + _ctx.issue.id, { method: 'PUT', headers: apiHeaders(), body: JSON.stringify(body) })
362
+ .then(function() { _ctx.reload(); _ctx.onAfterAction(); });
363
+ }
364
+
365
+ async function deleteIssue() {
366
+ if (!await showConfirm('Delete this issue?')) return;
367
+ fetch('/api/issues/' + _ctx.issue.id, { method: 'DELETE' })
368
+ .then(function(res) {
369
+ if (res.ok) { showToast('Issue deleted', 'success'); history.back(); }
370
+ else showToast('Only open issues can be deleted', 'error');
371
+ });
372
+ }
373
+
374
+ function closeWithComment() {
375
+ var body = document.getElementById('ir-comment-input').value.trim();
376
+ var p = Promise.resolve();
377
+ if (body) {
378
+ p = fetch('/api/issues/' + _ctx.issue.id + '/comments', { method: 'POST', headers: apiHeaders(), body: JSON.stringify({ author_id: 'user', body: body }) });
379
+ }
380
+ p.then(function() {
381
+ return fetch('/api/issues/' + _ctx.issue.id, { method: 'PUT', headers: apiHeaders(), body: JSON.stringify({ status: 'closed', actor: 'user' }) });
382
+ }).then(function() {
383
+ showToast(body ? 'Comment added and issue closed' : 'Issue closed', 'success');
384
+ _ctx.reload();
385
+ _ctx.onAfterAction();
386
+ });
387
+ }
388
+
389
+ function reopenWithComment() {
390
+ var body = document.getElementById('ir-comment-input').value.trim();
391
+ var p = Promise.resolve();
392
+ if (body) {
393
+ p = fetch('/api/issues/' + _ctx.issue.id + '/comments', { method: 'POST', headers: apiHeaders(), body: JSON.stringify({ author_id: 'user', body: body }) });
394
+ }
395
+ p.then(function() {
396
+ return fetch('/api/issues/' + _ctx.issue.id, { method: 'PUT', headers: apiHeaders(), body: JSON.stringify({ status: 'open', actor: 'user' }) });
397
+ }).then(function() {
398
+ showToast(body ? 'Comment added and issue reopened' : 'Issue reopened', 'success');
399
+ _ctx.reload();
400
+ _ctx.onAfterAction();
401
+ });
402
+ }
403
+
404
+ function addComment() {
405
+ var body = document.getElementById('ir-comment-input').value.trim();
406
+ if (!body) return;
407
+ fetch('/api/issues/' + _ctx.issue.id + '/comments', { method: 'POST', headers: apiHeaders(), body: JSON.stringify({ author_id: 'user', body: body }) })
408
+ .then(function(res) { if (res.ok) showToast('Comment added', 'success'); _ctx.reload(); });
409
+ }
410
+
411
+ function editComment(cid) {
412
+ var c = (_ctx.issue.comments || []).find(function(x) { return x.id === cid; });
413
+ if (!c) return;
414
+ var el = document.getElementById('ir-comment-body-' + cid);
415
+ if (!el) return;
416
+ el.innerHTML = '<textarea id="ir-edit-comment-' + cid + '" rows="4" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-size:13px;font-family:inherit">' + esc(c.body) + '</textarea>' +
417
+ '<div style="display:flex;gap:8px;margin-top:8px;justify-content:flex-end">' +
418
+ '<button class="btn btn-sm" onclick="IssueRenderer._ctx.reload()">Cancel</button>' +
419
+ '<button class="btn btn-sm btn-primary" onclick="IssueRenderer.saveComment(\'' + cid + '\')">Save</button>' +
420
+ '</div>';
421
+ }
422
+
423
+ function saveComment(cid) {
424
+ var v = document.getElementById('ir-edit-comment-' + cid);
425
+ if (!v || !v.value) return;
426
+ fetch('/api/comments/' + cid, { method: 'PUT', headers: apiHeaders(), body: JSON.stringify({ body: v.value }) })
427
+ .then(function(res) { if (res.ok) showToast('Comment saved', 'success'); _ctx.reload(); });
428
+ }
429
+
430
+ async function deleteComment(cid) {
431
+ if (!await showConfirm('Delete this comment?')) return;
432
+ fetch('/api/comments/' + cid, { method: 'DELETE' })
433
+ .then(function() { _ctx.reload(); });
434
+ }
435
+
436
+ function toggleReaction(type, id, emoji) {
437
+ fetch('/api/reactions/' + type + '/' + id, { method: 'POST', headers: apiHeaders(), body: JSON.stringify({ user_id: 'user', emoji: emoji }) })
438
+ .then(function() { _ctx.reload(); });
439
+ }
440
+
441
+ function showAddRelation() {
442
+ var existing = document.getElementById('ir-add-relation-dialog');
443
+ if (existing) { existing.remove(); return; }
444
+ var div = document.createElement('div');
445
+ div.id = 'ir-add-relation-dialog';
446
+ div.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--header-bg);border:1px solid var(--border);border-radius:8px;padding:16px;z-index:200;box-shadow:0 4px 12px rgba(0,0,0,0.3);min-width:300px';
447
+ div.innerHTML =
448
+ '<div style="font-weight:600;margin-bottom:12px">Add Dependency</div>' +
449
+ '<div style="margin-bottom:8px"><label style="font-size:12px;color:var(--text-secondary)">Type</label>' +
450
+ '<select id="ir-rel-type" style="width:100%;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-size:12px;margin-top:2px">' +
451
+ '<option value="blocks">This issue blocks...</option>' +
452
+ '<option value="blocked_by">This issue is blocked by...</option>' +
453
+ '<option value="related_to">Related to...</option>' +
454
+ '</select></div>' +
455
+ '<div style="margin-bottom:12px"><label style="font-size:12px;color:var(--text-secondary)">Issue number (e.g. 42)</label>' +
456
+ '<input type="text" id="ir-rel-target" placeholder="#" style="width:100%;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-size:12px;margin-top:2px"></div>' +
457
+ '<div style="display:flex;gap:8px;justify-content:flex-end">' +
458
+ '<button class="btn btn-sm" onclick="document.getElementById(\'ir-add-relation-dialog\').remove()">Cancel</button>' +
459
+ '<button class="btn btn-sm btn-primary" onclick="IssueRenderer.addRelation()">Add</button>' +
460
+ '</div>';
461
+ document.body.appendChild(div);
462
+ document.getElementById('ir-rel-target').focus();
463
+ }
464
+
465
+ function addRelation() {
466
+ var typeSelect = document.getElementById('ir-rel-type');
467
+ var targetInput = document.getElementById('ir-rel-target');
468
+ if (!typeSelect || !targetInput) return;
469
+ var relType = typeSelect.value;
470
+ var targetNum = targetInput.value.replace('#', '').trim();
471
+ if (!targetNum) return;
472
+
473
+ // For blocked_by: we need to reverse — from=target blocks to=this
474
+ // For the API: POST /api/issues/:id/relations with {type, target_issue_id}
475
+ // The API expects: from_issue_id = :id, to_issue_id = target_issue_id for "blocks"
476
+ // For "blocked_by", we need to call from the target's perspective
477
+ var issueId = _ctx.issue.id;
478
+ var projectId = _ctx.issue.project_id;
479
+
480
+ // First resolve issue number to ID
481
+ fetch('/api/projects/' + projectId + '/issues/number/' + targetNum, { headers: apiHeaders() })
482
+ .then(function(res) { if (!res.ok) throw new Error('Issue not found'); return res.json(); })
483
+ .then(function(targetIssue) {
484
+ var fromId, toId, apiType;
485
+ if (relType === 'blocked_by') {
486
+ // Target blocks this issue
487
+ fromId = targetIssue.id;
488
+ toId = issueId;
489
+ apiType = 'blocks';
490
+ return fetch('/api/issues/' + fromId + '/relations', {
491
+ method: 'POST', headers: apiHeaders(),
492
+ body: JSON.stringify({ type: apiType, target_issue_id: toId, actor: 'user' })
493
+ });
494
+ } else {
495
+ apiType = relType;
496
+ return fetch('/api/issues/' + issueId + '/relations', {
497
+ method: 'POST', headers: apiHeaders(),
498
+ body: JSON.stringify({ type: apiType, target_issue_id: targetIssue.id, actor: 'user' })
499
+ });
500
+ }
501
+ })
502
+ .then(function(res) {
503
+ if (!res.ok) return res.json().then(function(e) { throw new Error(e.error || 'Failed'); });
504
+ var dialog = document.getElementById('ir-add-relation-dialog');
505
+ if (dialog) dialog.remove();
506
+ showToast('Dependency added', 'success');
507
+ _ctx.reload();
508
+ })
509
+ .catch(function(err) { showToast(err.message, 'error'); });
510
+ }
511
+
512
+ function removeRelation(relationId) {
513
+ fetch('/api/issues/' + _ctx.issue.id + '/relations/' + relationId, { method: 'DELETE', headers: apiHeaders() })
514
+ .then(function(res) {
515
+ if (res.ok) { showToast('Dependency removed', 'success'); _ctx.reload(); }
516
+ else showToast('Failed to remove', 'error');
517
+ });
518
+ }
519
+
520
+ function showEmojiPicker(type, id) {
521
+ var picker = EMOJIS.map(function(e) { return '<span onclick="IssueRenderer.toggleReaction(\'' + type + '\',\'' + id + '\',\'' + e + '\')" style="cursor:pointer;font-size:18px;padding:2px">' + e + '</span>'; }).join('');
522
+ var el = document.getElementById('ir-emoji-picker');
523
+ if (el) { el.remove(); return; }
524
+ var div = document.createElement('div');
525
+ div.id = 'ir-emoji-picker';
526
+ div.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--header-bg);border:1px solid var(--border);border-radius:8px;padding:12px;z-index:200;box-shadow:0 4px 12px rgba(0,0,0,0.3)';
527
+ div.innerHTML = picker + '<div style="text-align:center;margin-top:8px"><button class="btn btn-sm" onclick="this.parentElement.parentElement.remove()">Close</button></div>';
528
+ document.body.appendChild(div);
529
+ }
530
+
531
+ return {
532
+ render: render,
533
+ renderMd: renderMd,
534
+ labelHtml: labelHtml,
535
+ statusIcon: statusIcon,
536
+ reactionBar: reactionBar,
537
+ // Actions (called from onclick handlers)
538
+ startEditTitle: startEditTitle,
539
+ cancelEditTitle: cancelEditTitle,
540
+ saveTitle: saveTitle,
541
+ startEditBody: startEditBody,
542
+ cancelEditBody: cancelEditBody,
543
+ saveBody: saveBody,
544
+ updateField: updateField,
545
+ deleteIssue: deleteIssue,
546
+ closeWithComment: closeWithComment,
547
+ reopenWithComment: reopenWithComment,
548
+ addComment: addComment,
549
+ editComment: editComment,
550
+ saveComment: saveComment,
551
+ deleteComment: deleteComment,
552
+ toggleReaction: toggleReaction,
553
+ showEmojiPicker: showEmojiPicker,
554
+ showAddRelation: showAddRelation,
555
+ addRelation: addRelation,
556
+ removeRelation: removeRelation,
557
+ _ctx: _ctx,
558
+ };
559
+ })();
@@ -0,0 +1,57 @@
1
+ // Parse URL: /issues/:uuid OR /projects/:pid/issues/:num
2
+ const pathParts = window.location.pathname.split('/').filter(Boolean);
3
+ let issueId = null;
4
+ let projectId = null;
5
+ let issueNum = null;
6
+ if (pathParts[0] === 'issues') { issueId = pathParts[1]; }
7
+ else if (pathParts[0] === 'projects' && pathParts[2] === 'issues') { projectId = pathParts[1]; issueNum = pathParts[3]; }
8
+
9
+ let issueData = null;
10
+ let agentsData = [];
11
+
12
+ async function loadIssue() {
13
+ let data;
14
+ if (issueId) {
15
+ const res = await fetch(`/api/issues/${issueId}`, { headers: apiHeaders() });
16
+ if (!res.ok) { document.getElementById('issue-page').innerHTML = '<div class="empty-state">Issue not found.</div>'; return; }
17
+ data = await res.json();
18
+ } else if (projectId && issueNum) {
19
+ const res = await fetch(`/api/projects/${projectId}/issues/number/${issueNum}`, { headers: apiHeaders() });
20
+ if (!res.ok) { document.getElementById('issue-page').innerHTML = '<div class="empty-state">Issue not found.</div>'; return; }
21
+ data = await res.json();
22
+ }
23
+ issueData = data; issueId = data.id;
24
+
25
+ // Fetch agents and project info in parallel
26
+ const [agentsRes, projectRes] = await Promise.allSettled([
27
+ fetch(`/api/projects/${data.project_id}/agents`, { headers: apiHeaders() }),
28
+ fetch(`/api/projects/${data.project_id}`, { headers: apiHeaders() })
29
+ ]);
30
+ if (agentsRes.status === 'fulfilled' && agentsRes.value.ok) agentsData = await agentsRes.value.json();
31
+
32
+ document.getElementById('project-link').href = `/projects/${data.project_id}`;
33
+ document.getElementById('issues-link').href = `/projects/${data.project_id}#issues`;
34
+ if (projectRes.status === 'fulfilled' && projectRes.value.ok) { const p = await projectRes.value.json(); document.getElementById('project-link').textContent = p.name; }
35
+ document.getElementById('issue-title-breadcrumb').textContent = `#${data.number} ${data.title}`;
36
+ document.title = `#${data.number} ${data.title} - Argus`;
37
+
38
+ IssueRenderer.render(issueData, agentsData, document.getElementById('issue-page'), {
39
+ reload: loadIssue,
40
+ });
41
+ setupIssueWS();
42
+ }
43
+
44
+ loadIssue();
45
+
46
+ // Connect to project WebSocket for real-time comment updates
47
+ let _issueEvents = null;
48
+ function setupIssueWS() {
49
+ if (!issueData || !issueData.project_id || _issueEvents) return;
50
+ _issueEvents = connectProjectEvents(issueData.project_id);
51
+ _issueEvents.on('comment_added', function(data) {
52
+ if (data.issueId === issueId) loadIssue();
53
+ });
54
+ _issueEvents.on('issue_updated', function(data) {
55
+ if (data.issue && data.issue.id === issueId) loadIssue();
56
+ });
57
+ }