open-coleslaw 0.5.2 → 0.5.3
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/dist/index.js +287 -118
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -964,6 +964,38 @@ ${previousMinutes.content}
|
|
|
964
964
|
}
|
|
965
965
|
};
|
|
966
966
|
|
|
967
|
+
// src/orchestrator/event-bus.ts
|
|
968
|
+
import { EventEmitter } from "events";
|
|
969
|
+
var EventBus = class {
|
|
970
|
+
emitter = new EventEmitter();
|
|
971
|
+
constructor() {
|
|
972
|
+
this.emitter.setMaxListeners(50);
|
|
973
|
+
}
|
|
974
|
+
emitAgentEvent(event) {
|
|
975
|
+
this.emitter.emit("agent_event", event);
|
|
976
|
+
}
|
|
977
|
+
on(event, listener) {
|
|
978
|
+
this.emitter.on(event, listener);
|
|
979
|
+
}
|
|
980
|
+
off(event, listener) {
|
|
981
|
+
this.emitter.off(event, listener);
|
|
982
|
+
}
|
|
983
|
+
once(event, listener) {
|
|
984
|
+
this.emitter.once(event, listener);
|
|
985
|
+
}
|
|
986
|
+
removeAllListeners(event) {
|
|
987
|
+
if (event) {
|
|
988
|
+
this.emitter.removeAllListeners(event);
|
|
989
|
+
} else {
|
|
990
|
+
this.emitter.removeAllListeners();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
listenerCount(event) {
|
|
994
|
+
return this.emitter.listenerCount(event);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
var eventBus = new EventBus();
|
|
998
|
+
|
|
967
999
|
// src/tools/start-meeting.ts
|
|
968
1000
|
var startMeetingSchema = {
|
|
969
1001
|
topic: z.string().describe("Meeting topic"),
|
|
@@ -987,6 +1019,14 @@ async function startMeetingHandler({
|
|
|
987
1019
|
departments,
|
|
988
1020
|
meetingType
|
|
989
1021
|
});
|
|
1022
|
+
eventBus.emitAgentEvent({
|
|
1023
|
+
kind: "meeting_started",
|
|
1024
|
+
meetingId: result.meetingId,
|
|
1025
|
+
meetingType: result.meetingType,
|
|
1026
|
+
topic: result.topic,
|
|
1027
|
+
agenda: [...result.agenda],
|
|
1028
|
+
participants: [...result.departments]
|
|
1029
|
+
});
|
|
990
1030
|
return {
|
|
991
1031
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
992
1032
|
};
|
|
@@ -1244,40 +1284,6 @@ async function getAgentTreeHandler() {
|
|
|
1244
1284
|
|
|
1245
1285
|
// src/tools/respond-to-mention.ts
|
|
1246
1286
|
import { z as z5 } from "zod";
|
|
1247
|
-
|
|
1248
|
-
// src/orchestrator/event-bus.ts
|
|
1249
|
-
import { EventEmitter } from "events";
|
|
1250
|
-
var EventBus = class {
|
|
1251
|
-
emitter = new EventEmitter();
|
|
1252
|
-
constructor() {
|
|
1253
|
-
this.emitter.setMaxListeners(50);
|
|
1254
|
-
}
|
|
1255
|
-
emitAgentEvent(event) {
|
|
1256
|
-
this.emitter.emit("agent_event", event);
|
|
1257
|
-
}
|
|
1258
|
-
on(event, listener) {
|
|
1259
|
-
this.emitter.on(event, listener);
|
|
1260
|
-
}
|
|
1261
|
-
off(event, listener) {
|
|
1262
|
-
this.emitter.off(event, listener);
|
|
1263
|
-
}
|
|
1264
|
-
once(event, listener) {
|
|
1265
|
-
this.emitter.once(event, listener);
|
|
1266
|
-
}
|
|
1267
|
-
removeAllListeners(event) {
|
|
1268
|
-
if (event) {
|
|
1269
|
-
this.emitter.removeAllListeners(event);
|
|
1270
|
-
} else {
|
|
1271
|
-
this.emitter.removeAllListeners();
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
listenerCount(event) {
|
|
1275
|
-
return this.emitter.listenerCount(event);
|
|
1276
|
-
}
|
|
1277
|
-
};
|
|
1278
|
-
var eventBus = new EventBus();
|
|
1279
|
-
|
|
1280
|
-
// src/tools/respond-to-mention.ts
|
|
1281
1287
|
var respondToMentionSchema = {
|
|
1282
1288
|
mentionId: z5.string().describe("ID of the mention to respond to"),
|
|
1283
1289
|
decision: z5.string().describe("The decision made by the user"),
|
|
@@ -2765,21 +2771,36 @@ var MeetingRunner = class {
|
|
|
2765
2771
|
// src/tools/add-transcript.ts
|
|
2766
2772
|
var addTranscriptSchema = {
|
|
2767
2773
|
meetingId: z13.string().describe("Meeting ID"),
|
|
2768
|
-
speakerRole: z13.string().describe('Role of the speaker (e.g. "
|
|
2769
|
-
agendaItemIndex: z13.number().describe("Agenda item index (0-based). Use -1 for opening statements, -2 for synthesis"),
|
|
2774
|
+
speakerRole: z13.string().describe('Role of the speaker (e.g. "planner", "architect", "engineer", "user")'),
|
|
2775
|
+
agendaItemIndex: z13.number().describe("Agenda item index (0-based). Use -1 for opening statements, -2 for synthesis, -3 for user interjections."),
|
|
2770
2776
|
roundNumber: z13.number().describe("Discussion round number (0 for opening/synthesis)"),
|
|
2771
|
-
content: z13.string().describe("The transcript content from the speaker")
|
|
2777
|
+
content: z13.string().describe("The transcript content from the speaker"),
|
|
2778
|
+
stance: z13.enum(["agree", "disagree", "speaking"]).optional().describe("Optional stance \u2014 used when the speaker is answering a consensus check.")
|
|
2772
2779
|
};
|
|
2773
2780
|
async function addTranscriptHandler({
|
|
2774
2781
|
meetingId,
|
|
2775
2782
|
speakerRole,
|
|
2776
2783
|
agendaItemIndex,
|
|
2777
2784
|
roundNumber,
|
|
2778
|
-
content
|
|
2785
|
+
content,
|
|
2786
|
+
stance
|
|
2779
2787
|
}) {
|
|
2780
2788
|
try {
|
|
2781
2789
|
const runner = new MeetingRunner(meetingId);
|
|
2782
2790
|
const entry = runner.addTranscript(speakerRole, agendaItemIndex, roundNumber, content);
|
|
2791
|
+
eventBus.emitAgentEvent({
|
|
2792
|
+
kind: "transcript_added",
|
|
2793
|
+
meetingId,
|
|
2794
|
+
comment: {
|
|
2795
|
+
id: entry.id,
|
|
2796
|
+
speakerRole,
|
|
2797
|
+
agendaItemIndex,
|
|
2798
|
+
roundNumber,
|
|
2799
|
+
content,
|
|
2800
|
+
stance: stance ?? "speaking",
|
|
2801
|
+
createdAt: entry.createdAt
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2783
2804
|
const result = {
|
|
2784
2805
|
success: true,
|
|
2785
2806
|
entryId: entry.id,
|
|
@@ -2814,6 +2835,16 @@ async function generateMinutesHandler({
|
|
|
2814
2835
|
const runner = new MeetingRunner(meetingId);
|
|
2815
2836
|
const minutesId = await runner.generateMinutes();
|
|
2816
2837
|
const minutes = getMinutesByMeeting(meetingId);
|
|
2838
|
+
if (minutes) {
|
|
2839
|
+
const decisions = extractSection(minutes.content, "Decisions");
|
|
2840
|
+
const actionItems = (minutes.actionItems ?? []).map((a) => a.title || a.description);
|
|
2841
|
+
eventBus.emitAgentEvent({
|
|
2842
|
+
kind: "minutes_finalized",
|
|
2843
|
+
meetingId,
|
|
2844
|
+
decisions,
|
|
2845
|
+
actionItems
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2817
2848
|
const result = {
|
|
2818
2849
|
minutesId,
|
|
2819
2850
|
meetingId,
|
|
@@ -2832,6 +2863,22 @@ async function generateMinutesHandler({
|
|
|
2832
2863
|
};
|
|
2833
2864
|
}
|
|
2834
2865
|
}
|
|
2866
|
+
function extractSection(md, heading) {
|
|
2867
|
+
const lines = md.split("\n");
|
|
2868
|
+
const out = [];
|
|
2869
|
+
let inSection = false;
|
|
2870
|
+
for (const line of lines) {
|
|
2871
|
+
const h = line.match(/^##\s+(.+)$/);
|
|
2872
|
+
if (h) {
|
|
2873
|
+
inSection = h[1].trim().toLowerCase().startsWith(heading.toLowerCase());
|
|
2874
|
+
continue;
|
|
2875
|
+
}
|
|
2876
|
+
if (!inSection) continue;
|
|
2877
|
+
const m = line.match(/^\s*[-*]\s+(.*\S)\s*$/);
|
|
2878
|
+
if (m) out.push(m[1]);
|
|
2879
|
+
}
|
|
2880
|
+
return out;
|
|
2881
|
+
}
|
|
2835
2882
|
|
|
2836
2883
|
// src/server.ts
|
|
2837
2884
|
function createServer() {
|
|
@@ -3104,11 +3151,61 @@ html, body {
|
|
|
3104
3151
|
letter-spacing: 0.5px;
|
|
3105
3152
|
}
|
|
3106
3153
|
.past-meeting {
|
|
3107
|
-
padding:
|
|
3154
|
+
padding: 8px 10px;
|
|
3108
3155
|
font-size: 11px;
|
|
3109
3156
|
color: var(--text2);
|
|
3110
3157
|
border-left: 2px solid var(--border);
|
|
3111
3158
|
margin-bottom: 4px;
|
|
3159
|
+
cursor: pointer;
|
|
3160
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
3161
|
+
border-radius: 0 4px 4px 0;
|
|
3162
|
+
}
|
|
3163
|
+
.past-meeting:hover {
|
|
3164
|
+
background: rgba(0,240,255,0.04);
|
|
3165
|
+
border-left-color: var(--cyan);
|
|
3166
|
+
color: var(--text);
|
|
3167
|
+
}
|
|
3168
|
+
.past-meeting.viewing {
|
|
3169
|
+
background: rgba(0,240,255,0.08);
|
|
3170
|
+
border-left-color: var(--cyan);
|
|
3171
|
+
color: var(--cyan);
|
|
3172
|
+
}
|
|
3173
|
+
.past-meeting .pm-title {
|
|
3174
|
+
font-weight: 500;
|
|
3175
|
+
display: block;
|
|
3176
|
+
margin-bottom: 2px;
|
|
3177
|
+
}
|
|
3178
|
+
.past-meeting .pm-meta {
|
|
3179
|
+
font-size: 10px;
|
|
3180
|
+
opacity: 0.7;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
/* viewing-past banner in the main area */
|
|
3184
|
+
#viewing-banner {
|
|
3185
|
+
display: none;
|
|
3186
|
+
padding: 8px 24px;
|
|
3187
|
+
background: rgba(168,85,247,0.1);
|
|
3188
|
+
border-bottom: 1px solid rgba(168,85,247,0.3);
|
|
3189
|
+
font-size: 12px;
|
|
3190
|
+
color: var(--purple);
|
|
3191
|
+
display: none;
|
|
3192
|
+
align-items: center;
|
|
3193
|
+
justify-content: space-between;
|
|
3194
|
+
}
|
|
3195
|
+
#viewing-banner.show { display: flex; }
|
|
3196
|
+
#viewing-banner button {
|
|
3197
|
+
background: transparent;
|
|
3198
|
+
border: 1px solid var(--purple);
|
|
3199
|
+
color: var(--purple);
|
|
3200
|
+
font-family: var(--font);
|
|
3201
|
+
font-size: 11px;
|
|
3202
|
+
padding: 4px 12px;
|
|
3203
|
+
border-radius: 4px;
|
|
3204
|
+
cursor: pointer;
|
|
3205
|
+
}
|
|
3206
|
+
#viewing-banner button:hover {
|
|
3207
|
+
background: var(--purple);
|
|
3208
|
+
color: var(--bg);
|
|
3112
3209
|
}
|
|
3113
3210
|
|
|
3114
3211
|
/* Main thread area */
|
|
@@ -3319,6 +3416,10 @@ html, body {
|
|
|
3319
3416
|
</aside>
|
|
3320
3417
|
|
|
3321
3418
|
<main id="main">
|
|
3419
|
+
<div id="viewing-banner">
|
|
3420
|
+
<span id="viewing-banner-text">Viewing a past meeting</span>
|
|
3421
|
+
<button type="button" onclick="backToLive()">\u2190 Back to live meeting</button>
|
|
3422
|
+
</div>
|
|
3322
3423
|
<div id="meeting-header">
|
|
3323
3424
|
<div id="meeting-title">No meeting in progress</div>
|
|
3324
3425
|
<div id="meeting-meta">Waiting for orchestrator\u2026</div>
|
|
@@ -3343,11 +3444,18 @@ html, body {
|
|
|
3343
3444
|
// State
|
|
3344
3445
|
// -----------------------------------------------------------------
|
|
3345
3446
|
const state = {
|
|
3346
|
-
sessions: new Map(), // sessionId \u2192 SessionSnapshot
|
|
3447
|
+
sessions: new Map(), // sessionId (== projectPath) \u2192 SessionSnapshot
|
|
3347
3448
|
activeSessionId: null,
|
|
3449
|
+
viewedMeetingId: null, // null \u2192 show live meeting; else show that past meeting
|
|
3348
3450
|
ws: null,
|
|
3349
3451
|
};
|
|
3350
3452
|
|
|
3453
|
+
function backToLive() {
|
|
3454
|
+
state.viewedMeetingId = null;
|
|
3455
|
+
renderAll();
|
|
3456
|
+
}
|
|
3457
|
+
window.backToLive = backToLive;
|
|
3458
|
+
|
|
3351
3459
|
const AVATARS = {
|
|
3352
3460
|
planner: '\u{1F4CC}',
|
|
3353
3461
|
architect: '\u{1F3DB}',
|
|
@@ -3508,7 +3616,7 @@ function renderTabs() {
|
|
|
3508
3616
|
if (sid === state.activeSessionId) btn.classList.add('active');
|
|
3509
3617
|
if (!s.isActive) btn.classList.add('inactive');
|
|
3510
3618
|
btn.textContent = s.displayName + (s.currentMeeting ? ' \u2022' : '');
|
|
3511
|
-
btn.onclick = () => { state.activeSessionId = sid; renderAll(); };
|
|
3619
|
+
btn.onclick = () => { state.activeSessionId = sid; state.viewedMeetingId = null; renderAll(); };
|
|
3512
3620
|
bar.appendChild(btn);
|
|
3513
3621
|
}
|
|
3514
3622
|
}
|
|
@@ -3541,7 +3649,17 @@ function renderSidebar() {
|
|
|
3541
3649
|
for (const m of s.pastMeetings) {
|
|
3542
3650
|
const row = document.createElement('div');
|
|
3543
3651
|
row.className = 'past-meeting';
|
|
3544
|
-
|
|
3652
|
+
if (state.viewedMeetingId === m.meetingId) row.classList.add('viewing');
|
|
3653
|
+
const title = document.createElement('span');
|
|
3654
|
+
title.className = 'pm-title';
|
|
3655
|
+
title.textContent = m.topic;
|
|
3656
|
+
const meta = document.createElement('span');
|
|
3657
|
+
meta.className = 'pm-meta';
|
|
3658
|
+
meta.textContent =
|
|
3659
|
+
m.meetingType.toUpperCase() + ' \xB7 ' + (m.decisions?.length || 0) + ' decisions';
|
|
3660
|
+
row.appendChild(title);
|
|
3661
|
+
row.appendChild(meta);
|
|
3662
|
+
row.onclick = () => { state.viewedMeetingId = m.meetingId; renderAll(); };
|
|
3545
3663
|
pastEl.appendChild(row);
|
|
3546
3664
|
}
|
|
3547
3665
|
}
|
|
@@ -3555,8 +3673,24 @@ function renderMain() {
|
|
|
3555
3673
|
const thread = document.getElementById('thread');
|
|
3556
3674
|
const input = document.getElementById('comment-input');
|
|
3557
3675
|
const send = document.getElementById('comment-send');
|
|
3676
|
+
const banner = document.getElementById('viewing-banner');
|
|
3677
|
+
const bannerText = document.getElementById('viewing-banner-text');
|
|
3558
3678
|
|
|
3559
|
-
|
|
3679
|
+
// Resolve which meeting to render: viewedMeetingId (past) or current
|
|
3680
|
+
let m = null;
|
|
3681
|
+
let isPast = false;
|
|
3682
|
+
if (s) {
|
|
3683
|
+
if (state.viewedMeetingId) {
|
|
3684
|
+
m = (s.pastMeetings || []).find((x) => x.meetingId === state.viewedMeetingId) || null;
|
|
3685
|
+
isPast = !!m;
|
|
3686
|
+
// If the past meeting disappeared (retention eviction), fall back to live.
|
|
3687
|
+
if (!m) state.viewedMeetingId = null;
|
|
3688
|
+
}
|
|
3689
|
+
if (!m) m = s.currentMeeting;
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
if (!m) {
|
|
3693
|
+
banner.classList.remove('show');
|
|
3560
3694
|
title.textContent = 'No meeting in progress';
|
|
3561
3695
|
meta.textContent = s ? 'Session ready' : 'No active session';
|
|
3562
3696
|
agenda.innerHTML = '';
|
|
@@ -3566,9 +3700,15 @@ function renderMain() {
|
|
|
3566
3700
|
return;
|
|
3567
3701
|
}
|
|
3568
3702
|
|
|
3569
|
-
|
|
3703
|
+
if (isPast) {
|
|
3704
|
+
banner.classList.add('show');
|
|
3705
|
+
bannerText.textContent = '\u{1F4D6} Viewing past meeting \xB7 ' + m.topic;
|
|
3706
|
+
} else {
|
|
3707
|
+
banner.classList.remove('show');
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3570
3710
|
title.textContent = m.topic;
|
|
3571
|
-
meta.textContent = [m.meetingType.toUpperCase(), m.status, m.participants.join(', ')].join(' \xB7 ');
|
|
3711
|
+
meta.textContent = [m.meetingType.toUpperCase(), m.status, (m.participants || []).join(', ')].join(' \xB7 ');
|
|
3572
3712
|
agenda.innerHTML = '';
|
|
3573
3713
|
(m.agenda || []).forEach((item, i) => {
|
|
3574
3714
|
const chip = document.createElement('span');
|
|
@@ -3586,8 +3726,13 @@ function renderMain() {
|
|
|
3586
3726
|
}
|
|
3587
3727
|
thread.scrollTop = thread.scrollHeight;
|
|
3588
3728
|
|
|
3589
|
-
|
|
3590
|
-
|
|
3729
|
+
// Comment box: disabled when viewing a past meeting or when current meeting is completed
|
|
3730
|
+
const disabled = isPast || m.status === 'completed';
|
|
3731
|
+
input.disabled = disabled;
|
|
3732
|
+
send.disabled = disabled;
|
|
3733
|
+
input.placeholder = isPast
|
|
3734
|
+
? 'Comments disabled \u2014 this is a past meeting'
|
|
3735
|
+
: 'Add a comment to this meeting\u2026 (Enter to send, Shift+Enter for newline)';
|
|
3591
3736
|
}
|
|
3592
3737
|
|
|
3593
3738
|
function commentEl(c) {
|
|
@@ -3665,31 +3810,54 @@ connect();
|
|
|
3665
3810
|
|
|
3666
3811
|
// src/dashboard/state-bridge.ts
|
|
3667
3812
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
3668
|
-
var MAX_PAST_MEETINGS =
|
|
3813
|
+
var MAX_PAST_MEETINGS = 20;
|
|
3669
3814
|
var EVENT_DEBOUNCE_MS = 100;
|
|
3670
3815
|
var StateBridge = class extends EventEmitter2 {
|
|
3671
|
-
|
|
3816
|
+
// projectPath → ProjectState (single row per project, merged across terminals)
|
|
3817
|
+
projects = /* @__PURE__ */ new Map();
|
|
3818
|
+
// terminal sessionId → projectPath (for event routing)
|
|
3819
|
+
sessionToProject = /* @__PURE__ */ new Map();
|
|
3672
3820
|
debounceTimers = /* @__PURE__ */ new Map();
|
|
3673
3821
|
pendingEvents = /* @__PURE__ */ new Map();
|
|
3674
3822
|
// ---- Session lifecycle --------------------------------------------------
|
|
3675
3823
|
registerSession(info) {
|
|
3676
|
-
const
|
|
3677
|
-
|
|
3824
|
+
const existing = this.projects.get(info.projectPath);
|
|
3825
|
+
if (existing) {
|
|
3826
|
+
existing.activeSessionIds.add(info.sessionId);
|
|
3827
|
+
this.sessionToProject.set(info.sessionId, info.projectPath);
|
|
3828
|
+
logger.info(`Session reattached to existing project: ${existing.displayName} (${info.sessionId})`);
|
|
3829
|
+
this.emit(
|
|
3830
|
+
"broadcast",
|
|
3831
|
+
JSON.stringify({
|
|
3832
|
+
type: "session-registered",
|
|
3833
|
+
sessionId: info.projectPath,
|
|
3834
|
+
// wire sessionId = projectPath so UI tab is stable
|
|
3835
|
+
displayName: existing.displayName,
|
|
3836
|
+
projectPath: info.projectPath
|
|
3837
|
+
})
|
|
3838
|
+
);
|
|
3839
|
+
return existing.displayName;
|
|
3840
|
+
}
|
|
3841
|
+
const displayName = info.projectName;
|
|
3842
|
+
const state = {
|
|
3843
|
+
projectPath: info.projectPath,
|
|
3678
3844
|
projectName: info.projectName,
|
|
3679
3845
|
displayName,
|
|
3680
|
-
|
|
3681
|
-
isActive: true,
|
|
3846
|
+
activeSessionIds: /* @__PURE__ */ new Set([info.sessionId]),
|
|
3682
3847
|
currentMeeting: null,
|
|
3683
3848
|
pastMeetings: [],
|
|
3684
3849
|
mvps: [],
|
|
3685
3850
|
totalCost: 0
|
|
3686
|
-
}
|
|
3687
|
-
|
|
3851
|
+
};
|
|
3852
|
+
this.projects.set(info.projectPath, state);
|
|
3853
|
+
this.sessionToProject.set(info.sessionId, info.projectPath);
|
|
3854
|
+
logger.info(`Project registered: ${displayName} (${info.projectPath})`);
|
|
3688
3855
|
this.emit(
|
|
3689
3856
|
"broadcast",
|
|
3690
3857
|
JSON.stringify({
|
|
3691
3858
|
type: "session-registered",
|
|
3692
|
-
sessionId: info.
|
|
3859
|
+
sessionId: info.projectPath,
|
|
3860
|
+
// projectPath is the stable wire-side sessionId
|
|
3693
3861
|
displayName,
|
|
3694
3862
|
projectPath: info.projectPath
|
|
3695
3863
|
})
|
|
@@ -3697,34 +3865,39 @@ var StateBridge = class extends EventEmitter2 {
|
|
|
3697
3865
|
return displayName;
|
|
3698
3866
|
}
|
|
3699
3867
|
unregisterSession(sessionId) {
|
|
3700
|
-
const
|
|
3701
|
-
if (
|
|
3702
|
-
|
|
3703
|
-
|
|
3868
|
+
const projectPath = this.sessionToProject.get(sessionId);
|
|
3869
|
+
if (!projectPath) return;
|
|
3870
|
+
this.sessionToProject.delete(sessionId);
|
|
3871
|
+
const project = this.projects.get(projectPath);
|
|
3872
|
+
if (!project) return;
|
|
3873
|
+
project.activeSessionIds.delete(sessionId);
|
|
3874
|
+
if (project.activeSessionIds.size === 0) {
|
|
3875
|
+
logger.info(`Project deactivated: ${project.displayName}`);
|
|
3704
3876
|
this.emit(
|
|
3705
3877
|
"broadcast",
|
|
3706
3878
|
JSON.stringify({
|
|
3707
3879
|
type: "session-unregistered",
|
|
3708
|
-
sessionId
|
|
3880
|
+
sessionId: projectPath
|
|
3709
3881
|
})
|
|
3710
3882
|
);
|
|
3711
3883
|
}
|
|
3712
3884
|
}
|
|
3713
3885
|
// ---- Event handling -----------------------------------------------------
|
|
3714
3886
|
handleSessionEvent(sessionId, event) {
|
|
3715
|
-
const
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3887
|
+
const projectPath = this.sessionToProject.get(sessionId) ?? sessionId;
|
|
3888
|
+
const project = this.projects.get(projectPath);
|
|
3889
|
+
if (!project) return;
|
|
3890
|
+
this.applyEvent(project, event);
|
|
3891
|
+
if (!this.pendingEvents.has(projectPath)) {
|
|
3892
|
+
this.pendingEvents.set(projectPath, []);
|
|
3893
|
+
}
|
|
3894
|
+
this.pendingEvents.get(projectPath).push(event);
|
|
3895
|
+
if (!this.debounceTimers.has(projectPath)) {
|
|
3723
3896
|
this.debounceTimers.set(
|
|
3724
|
-
|
|
3897
|
+
projectPath,
|
|
3725
3898
|
setTimeout(() => {
|
|
3726
|
-
this.flushEvents(
|
|
3727
|
-
this.debounceTimers.delete(
|
|
3899
|
+
this.flushEvents(projectPath);
|
|
3900
|
+
this.debounceTimers.delete(projectPath);
|
|
3728
3901
|
}, EVENT_DEBOUNCE_MS)
|
|
3729
3902
|
);
|
|
3730
3903
|
}
|
|
@@ -3733,12 +3906,13 @@ var StateBridge = class extends EventEmitter2 {
|
|
|
3733
3906
|
getSnapshot() {
|
|
3734
3907
|
return {
|
|
3735
3908
|
type: "multi-snapshot",
|
|
3736
|
-
sessions: Array.from(this.
|
|
3737
|
-
(
|
|
3738
|
-
sessionId,
|
|
3909
|
+
sessions: Array.from(this.projects.values()).map(
|
|
3910
|
+
(s) => ({
|
|
3911
|
+
sessionId: s.projectPath,
|
|
3912
|
+
// stable wire-side id
|
|
3739
3913
|
displayName: s.displayName,
|
|
3740
3914
|
projectPath: s.projectPath,
|
|
3741
|
-
isActive: s.
|
|
3915
|
+
isActive: s.activeSessionIds.size > 0,
|
|
3742
3916
|
currentMeeting: s.currentMeeting,
|
|
3743
3917
|
pastMeetings: [...s.pastMeetings],
|
|
3744
3918
|
mvps: [...s.mvps],
|
|
@@ -3748,25 +3922,16 @@ var StateBridge = class extends EventEmitter2 {
|
|
|
3748
3922
|
};
|
|
3749
3923
|
}
|
|
3750
3924
|
// ---- Private helpers ----------------------------------------------------
|
|
3751
|
-
|
|
3752
|
-
const existing = Array.from(this.sessions.values()).map(
|
|
3753
|
-
(s) => s.displayName
|
|
3754
|
-
);
|
|
3755
|
-
if (!existing.includes(projectName)) return projectName;
|
|
3756
|
-
let i = 1;
|
|
3757
|
-
while (existing.includes(`${projectName} (${i})`)) i++;
|
|
3758
|
-
return `${projectName} (${i})`;
|
|
3759
|
-
}
|
|
3760
|
-
applyEvent(session, event) {
|
|
3925
|
+
applyEvent(project, event) {
|
|
3761
3926
|
switch (event.kind) {
|
|
3762
3927
|
case "meeting_started": {
|
|
3763
|
-
if (
|
|
3764
|
-
|
|
3765
|
-
if (
|
|
3766
|
-
|
|
3928
|
+
if (project.currentMeeting) {
|
|
3929
|
+
project.pastMeetings.unshift(project.currentMeeting);
|
|
3930
|
+
if (project.pastMeetings.length > MAX_PAST_MEETINGS) {
|
|
3931
|
+
project.pastMeetings.pop();
|
|
3767
3932
|
}
|
|
3768
3933
|
}
|
|
3769
|
-
|
|
3934
|
+
project.currentMeeting = {
|
|
3770
3935
|
meetingId: event.meetingId,
|
|
3771
3936
|
meetingType: event.meetingType,
|
|
3772
3937
|
topic: event.topic,
|
|
@@ -3775,7 +3940,7 @@ var StateBridge = class extends EventEmitter2 {
|
|
|
3775
3940
|
status: "in-progress",
|
|
3776
3941
|
phase: "opening",
|
|
3777
3942
|
comments: [],
|
|
3778
|
-
mvps: [],
|
|
3943
|
+
mvps: [...project.mvps],
|
|
3779
3944
|
decisions: [],
|
|
3780
3945
|
actionItems: [],
|
|
3781
3946
|
startedAt: Date.now(),
|
|
@@ -3784,36 +3949,40 @@ var StateBridge = class extends EventEmitter2 {
|
|
|
3784
3949
|
break;
|
|
3785
3950
|
}
|
|
3786
3951
|
case "transcript_added": {
|
|
3787
|
-
if (
|
|
3788
|
-
|
|
3952
|
+
if (project.currentMeeting?.meetingId === event.meetingId) {
|
|
3953
|
+
project.currentMeeting.comments.push(event.comment);
|
|
3954
|
+
} else {
|
|
3955
|
+
const past = project.pastMeetings.find((m) => m.meetingId === event.meetingId);
|
|
3956
|
+
if (past) past.comments.push(event.comment);
|
|
3789
3957
|
}
|
|
3790
3958
|
break;
|
|
3791
3959
|
}
|
|
3792
3960
|
case "round_advanced": {
|
|
3793
|
-
if (
|
|
3794
|
-
|
|
3961
|
+
if (project.currentMeeting?.meetingId === event.meetingId) {
|
|
3962
|
+
project.currentMeeting.phase = "discussion";
|
|
3795
3963
|
}
|
|
3796
3964
|
break;
|
|
3797
3965
|
}
|
|
3798
3966
|
case "consensus_checked": {
|
|
3799
|
-
if (
|
|
3800
|
-
|
|
3967
|
+
if (project.currentMeeting?.meetingId === event.meetingId) {
|
|
3968
|
+
project.currentMeeting.status = event.allAgreed ? "in-progress" : "awaiting-consensus";
|
|
3801
3969
|
}
|
|
3802
3970
|
break;
|
|
3803
3971
|
}
|
|
3804
3972
|
case "minutes_finalized": {
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3973
|
+
const target = project.currentMeeting?.meetingId === event.meetingId ? project.currentMeeting : project.pastMeetings.find((m) => m.meetingId === event.meetingId);
|
|
3974
|
+
if (target) {
|
|
3975
|
+
target.decisions = [...event.decisions];
|
|
3976
|
+
target.actionItems = [...event.actionItems];
|
|
3977
|
+
target.status = "completed";
|
|
3978
|
+
target.phase = "minutes-generation";
|
|
3979
|
+
target.completedAt = Date.now();
|
|
3811
3980
|
}
|
|
3812
3981
|
break;
|
|
3813
3982
|
}
|
|
3814
3983
|
case "user_comment_added": {
|
|
3815
|
-
if (
|
|
3816
|
-
const nextId =
|
|
3984
|
+
if (project.currentMeeting?.meetingId === event.meetingId) {
|
|
3985
|
+
const nextId = project.currentMeeting.comments.length + 1;
|
|
3817
3986
|
const userComment = {
|
|
3818
3987
|
id: nextId,
|
|
3819
3988
|
speakerRole: "user",
|
|
@@ -3823,19 +3992,19 @@ var StateBridge = class extends EventEmitter2 {
|
|
|
3823
3992
|
stance: "speaking",
|
|
3824
3993
|
createdAt: Date.now()
|
|
3825
3994
|
};
|
|
3826
|
-
|
|
3995
|
+
project.currentMeeting.comments.push(userComment);
|
|
3827
3996
|
}
|
|
3828
3997
|
break;
|
|
3829
3998
|
}
|
|
3830
3999
|
case "mvp_progress": {
|
|
3831
|
-
|
|
3832
|
-
if (
|
|
3833
|
-
|
|
4000
|
+
project.mvps = [...event.mvps];
|
|
4001
|
+
if (project.currentMeeting) {
|
|
4002
|
+
project.currentMeeting.mvps = [...event.mvps];
|
|
3834
4003
|
}
|
|
3835
4004
|
break;
|
|
3836
4005
|
}
|
|
3837
4006
|
case "cost_update": {
|
|
3838
|
-
|
|
4007
|
+
project.totalCost = event.totalCost;
|
|
3839
4008
|
break;
|
|
3840
4009
|
}
|
|
3841
4010
|
case "mention_created":
|
|
@@ -3843,19 +4012,19 @@ var StateBridge = class extends EventEmitter2 {
|
|
|
3843
4012
|
break;
|
|
3844
4013
|
}
|
|
3845
4014
|
}
|
|
3846
|
-
flushEvents(
|
|
3847
|
-
const events = this.pendingEvents.get(
|
|
3848
|
-
const
|
|
3849
|
-
if (!events || !
|
|
4015
|
+
flushEvents(projectPath) {
|
|
4016
|
+
const events = this.pendingEvents.get(projectPath);
|
|
4017
|
+
const project = this.projects.get(projectPath);
|
|
4018
|
+
if (!events || !project || events.length === 0) return;
|
|
3850
4019
|
const delta = {
|
|
3851
4020
|
type: "session-delta",
|
|
3852
|
-
sessionId,
|
|
3853
|
-
displayName:
|
|
4021
|
+
sessionId: projectPath,
|
|
4022
|
+
displayName: project.displayName,
|
|
3854
4023
|
timestamp: Date.now(),
|
|
3855
4024
|
events: [...events]
|
|
3856
4025
|
};
|
|
3857
4026
|
this.emit("broadcast", JSON.stringify(delta));
|
|
3858
|
-
this.pendingEvents.set(
|
|
4027
|
+
this.pendingEvents.set(projectPath, []);
|
|
3859
4028
|
}
|
|
3860
4029
|
};
|
|
3861
4030
|
|