letagents 0.12.22 → 0.12.23
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/mcp/server/runtime/room-api.js +4 -2
- package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -2
- package/dist/mcp/server/tools/messages/send-tool.js +2 -2
- package/dist/mcp/server/tools/messages/status-tool.js +2 -2
- package/package.json +2 -2
- package/shared/activation-routing.d.mts +1 -1
- package/shared/rooms/RoomsDirectory.vue +518 -0
- package/shared/rooms/directory.ts +84 -0
- package/shared/rooms/rooms-directory.css +757 -0
- package/shared/task-markdown-editing.d.mts +9 -0
- package/shared/task-markdown-editing.mjs +53 -0
- package/shared/task-markdown.css +32 -0
|
@@ -7,6 +7,8 @@ import { currentRoom, getCurrentSupervisedRoomAuthority } from "./room-state.js"
|
|
|
7
7
|
import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
|
|
8
8
|
import { currentWorkerCall } from "../../worker-call-context.js";
|
|
9
9
|
export async function roomScopedApiCall(input) {
|
|
10
|
+
// A successful send does not prove that preceding messages were read.
|
|
11
|
+
const preserveCursor = input.preserve_session_cursor || (input.options?.method ?? "GET").toUpperCase() !== "GET";
|
|
10
12
|
const supervised = hasSupervisedWorkerAuthority();
|
|
11
13
|
const exactRoomAuthority = supervised ? getCurrentSupervisedRoomAuthority() : null;
|
|
12
14
|
if (supervised && (!exactRoomAuthority || input.room_id !== exactRoomAuthority)) {
|
|
@@ -36,7 +38,7 @@ export async function roomScopedApiCall(input) {
|
|
|
36
38
|
try {
|
|
37
39
|
const result = await apiCall(input.room_path(apiRoomId), options);
|
|
38
40
|
if (!supervised) {
|
|
39
|
-
touchRoomSession(input.room_id,
|
|
41
|
+
touchRoomSession(input.room_id, preserveCursor ? undefined : getLastMessageId(result));
|
|
40
42
|
}
|
|
41
43
|
return result;
|
|
42
44
|
}
|
|
@@ -53,7 +55,7 @@ export async function roomScopedApiCall(input) {
|
|
|
53
55
|
const result = await apiCall(input.project_path(input.project_id), options);
|
|
54
56
|
if (input.room_id) {
|
|
55
57
|
if (!supervised) {
|
|
56
|
-
touchRoomSession(input.room_id,
|
|
58
|
+
touchRoomSession(input.room_id, preserveCursor ? undefined : getLastMessageId(result));
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
return result;
|
|
@@ -65,7 +65,7 @@ export function registerPostReasoningTool(server) {
|
|
|
65
65
|
publisher_agent_session_id: agentSession?.session_id ?? null,
|
|
66
66
|
});
|
|
67
67
|
milestoneMessageId = milestoneMessage.id;
|
|
68
|
-
touchCurrentRoom(
|
|
68
|
+
touchCurrentRoom();
|
|
69
69
|
}
|
|
70
70
|
await syncRoomPresence(effectiveLocalRoomId, identity, {
|
|
71
71
|
status: status ?? getRememberedRoomPresence(effectiveLocalRoomId, identity).status,
|
|
@@ -158,7 +158,7 @@ export function registerPostReasoningTool(server) {
|
|
|
158
158
|
});
|
|
159
159
|
milestoneMessageId =
|
|
160
160
|
typeof milestoneMessage.id === "string" ? milestoneMessage.id : null;
|
|
161
|
-
touchCurrentRoom(
|
|
161
|
+
touchCurrentRoom();
|
|
162
162
|
}
|
|
163
163
|
if (status) {
|
|
164
164
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
@@ -90,7 +90,7 @@ async function sendMessageFromTool(input) {
|
|
|
90
90
|
publisher_agent_key: agentSession?.agent_key ?? null,
|
|
91
91
|
publisher_agent_session_id: agentSession?.session_id ?? null,
|
|
92
92
|
});
|
|
93
|
-
touchCurrentRoom(
|
|
93
|
+
touchCurrentRoom();
|
|
94
94
|
return jsonToolResponse({
|
|
95
95
|
...message,
|
|
96
96
|
agent_identity: toPublicAgentIdentity(identity),
|
|
@@ -112,7 +112,7 @@ async function sendMessageFromTool(input) {
|
|
|
112
112
|
})),
|
|
113
113
|
},
|
|
114
114
|
});
|
|
115
|
-
touchCurrentRoom(
|
|
115
|
+
touchCurrentRoom();
|
|
116
116
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
|
|
117
117
|
// The `replied` receipt transition is server-owned: message creation marks
|
|
118
118
|
// the publisher's receipt on the reply target atomically with the reply
|
|
@@ -48,7 +48,7 @@ export function registerPostStatusTool(server) {
|
|
|
48
48
|
publisher_agent_key: agentSession?.agent_key ?? null,
|
|
49
49
|
publisher_agent_session_id: agentSession?.session_id ?? null,
|
|
50
50
|
});
|
|
51
|
-
touchCurrentRoom(
|
|
51
|
+
touchCurrentRoom();
|
|
52
52
|
return jsonToolResponse({
|
|
53
53
|
success: true,
|
|
54
54
|
status_posted: status,
|
|
@@ -76,7 +76,7 @@ export function registerPostStatusTool(server) {
|
|
|
76
76
|
}),
|
|
77
77
|
},
|
|
78
78
|
});
|
|
79
|
-
touchCurrentRoom(
|
|
79
|
+
touchCurrentRoom();
|
|
80
80
|
return jsonToolResponse({
|
|
81
81
|
success: true,
|
|
82
82
|
status_posted: status,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letagents",
|
|
3
|
-
"version": "0.12.
|
|
4
|
-
"description": "Let Agents Chat
|
|
3
|
+
"version": "0.12.23",
|
|
4
|
+
"description": "Let Agents Chat \u2014 MCP server for AI agent communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/mcp/server.js",
|
|
7
7
|
"bin": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type AgentMessageActivationDecision = "activate" | "silent" | "unclear";
|
|
2
|
-
export type AgentMessageActivationReason = "self_message" | "explicit_mention" | "explicit_other_mention" | "broadcast" | "reply_target" | "other_reply_target" | "thread_participant" | "task_owner" | "small_room" | "recent_conversation" | "system_event" | "unaddressed";
|
|
2
|
+
export type AgentMessageActivationReason = "self_message" | "explicit_mention" | "explicit_other_mention" | "broadcast" | "reply_target" | "other_reply_target" | "thread_participant" | "task_owner" | "small_room" | "recent_conversation" | "jev_routed" | "system_event" | "unaddressed";
|
|
3
3
|
/** Send-time human fallback only; never use this to re-route historical reads. */
|
|
4
4
|
export declare function humanConversationFallback(input: {
|
|
5
5
|
source: string | null;
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="rooms-directory" aria-label="Rooms">
|
|
3
|
+
<header class="rooms-heading">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="rooms-parent">
|
|
6
|
+
<span aria-hidden="true">⌂</span> {{ parentLabel }}
|
|
7
|
+
</p>
|
|
8
|
+
<h2>Rooms</h2>
|
|
9
|
+
<p class="rooms-intro">
|
|
10
|
+
Give a task or topic its own conversation. Bring the outcome back
|
|
11
|
+
here.
|
|
12
|
+
</p>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="rooms-heading-actions">
|
|
15
|
+
<button
|
|
16
|
+
v-if="canRefresh"
|
|
17
|
+
class="rooms-icon-button"
|
|
18
|
+
type="button"
|
|
19
|
+
aria-label="Refresh rooms"
|
|
20
|
+
@click="emit('refresh')"
|
|
21
|
+
>
|
|
22
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
23
|
+
<path
|
|
24
|
+
d="M20 7v5h-5M4 17v-5h5M6 7a7 7 0 0 1 12-1l2 3M4 15l2 3a7 7 0 0 0 12-1"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
</button>
|
|
28
|
+
<button
|
|
29
|
+
ref="newRoomButton"
|
|
30
|
+
class="rooms-button rooms-button-primary"
|
|
31
|
+
type="button"
|
|
32
|
+
:aria-expanded="composing"
|
|
33
|
+
:aria-controls="`${uid}-create`"
|
|
34
|
+
@click="toggleComposer"
|
|
35
|
+
>
|
|
36
|
+
<span aria-hidden="true">+</span> New room
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<Transition name="rooms-reveal">
|
|
42
|
+
<section
|
|
43
|
+
v-if="composing"
|
|
44
|
+
:id="`${uid}-create`"
|
|
45
|
+
class="rooms-composer"
|
|
46
|
+
aria-label="New room"
|
|
47
|
+
@keydown.esc.stop.prevent="closeComposer"
|
|
48
|
+
>
|
|
49
|
+
<div class="rooms-composer-heading">
|
|
50
|
+
<div>
|
|
51
|
+
<h3>Make space for focused work</h3>
|
|
52
|
+
<p>The conversation stays connected to {{ parentLabel }}.</p>
|
|
53
|
+
</div>
|
|
54
|
+
<button
|
|
55
|
+
class="rooms-icon-button"
|
|
56
|
+
type="button"
|
|
57
|
+
aria-label="Cancel new room"
|
|
58
|
+
:disabled="busy"
|
|
59
|
+
@click="closeComposer"
|
|
60
|
+
>
|
|
61
|
+
<span aria-hidden="true">×</span>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="rooms-mode" aria-label="Room purpose">
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
:aria-pressed="creationMode === 'topic'"
|
|
68
|
+
:disabled="busy"
|
|
69
|
+
@click="setCreationMode('topic')"
|
|
70
|
+
>
|
|
71
|
+
For a topic
|
|
72
|
+
</button>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
:aria-pressed="creationMode === 'task'"
|
|
76
|
+
:disabled="busy"
|
|
77
|
+
@click="setCreationMode('task')"
|
|
78
|
+
>
|
|
79
|
+
From a task
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
<form
|
|
83
|
+
v-if="creationMode === 'topic'"
|
|
84
|
+
class="rooms-topic-form"
|
|
85
|
+
@submit.prevent="createTopic"
|
|
86
|
+
>
|
|
87
|
+
<label :for="`${uid}-title`">Room name</label>
|
|
88
|
+
<div class="rooms-input-action">
|
|
89
|
+
<input
|
|
90
|
+
:id="`${uid}-title`"
|
|
91
|
+
ref="titleInput"
|
|
92
|
+
v-model="topicTitle"
|
|
93
|
+
placeholder="e.g. Plan the next release"
|
|
94
|
+
autocomplete="off"
|
|
95
|
+
:disabled="busy"
|
|
96
|
+
required
|
|
97
|
+
/>
|
|
98
|
+
<button
|
|
99
|
+
class="rooms-button rooms-button-primary"
|
|
100
|
+
type="submit"
|
|
101
|
+
:disabled="busy || !topicTitle.trim()"
|
|
102
|
+
>
|
|
103
|
+
{{ busy ? 'Creating…' : 'Create and open'
|
|
104
|
+
}}<span v-if="!busy" aria-hidden="true">↗</span>
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
<p class="rooms-hint">
|
|
108
|
+
A shared conversation, with its own agents and history. No task
|
|
109
|
+
required.
|
|
110
|
+
</p>
|
|
111
|
+
</form>
|
|
112
|
+
<div v-else class="rooms-task-picker">
|
|
113
|
+
<label class="rooms-search"
|
|
114
|
+
><svg viewBox="0 0 24 24" aria-hidden="true">
|
|
115
|
+
<circle cx="10.5" cy="10.5" r="6.5" />
|
|
116
|
+
<path d="m16 16 4 4" /></svg
|
|
117
|
+
><input
|
|
118
|
+
ref="taskSearchInput"
|
|
119
|
+
v-model="taskQuery"
|
|
120
|
+
type="search"
|
|
121
|
+
aria-label="Find a task"
|
|
122
|
+
placeholder="Find a task…"
|
|
123
|
+
:disabled="busy"
|
|
124
|
+
/></label>
|
|
125
|
+
<div
|
|
126
|
+
v-if="visibleTasks.length"
|
|
127
|
+
class="rooms-task-list"
|
|
128
|
+
aria-label="Choose a task"
|
|
129
|
+
>
|
|
130
|
+
<button
|
|
131
|
+
v-for="task in visibleTasks"
|
|
132
|
+
:key="task.id"
|
|
133
|
+
type="button"
|
|
134
|
+
:aria-pressed="selectedTaskId === task.id"
|
|
135
|
+
:disabled="busy"
|
|
136
|
+
@click="selectedTaskId = task.id"
|
|
137
|
+
>
|
|
138
|
+
<span class="rooms-task-radio" aria-hidden="true"></span
|
|
139
|
+
><span
|
|
140
|
+
><strong>{{ task.title }}</strong
|
|
141
|
+
><small>{{
|
|
142
|
+
task.roomId
|
|
143
|
+
? task.roomClosed
|
|
144
|
+
? 'Room closed · view its result'
|
|
145
|
+
: 'Room already open'
|
|
146
|
+
: taskStatusLabel(task.status)
|
|
147
|
+
}}</small></span
|
|
148
|
+
><span v-if="selectedTaskId === task.id" aria-hidden="true"
|
|
149
|
+
>✓</span
|
|
150
|
+
>
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
<p v-else class="rooms-task-empty">
|
|
154
|
+
{{
|
|
155
|
+
taskQuery
|
|
156
|
+
? 'No tasks match your search.'
|
|
157
|
+
: 'No open tasks yet. Add a task on the board, or start with a topic.'
|
|
158
|
+
}}
|
|
159
|
+
</p>
|
|
160
|
+
<div class="rooms-task-footer">
|
|
161
|
+
<p>
|
|
162
|
+
{{
|
|
163
|
+
selectedTask?.roomId
|
|
164
|
+
? 'This task already has a room. Continue the conversation there.'
|
|
165
|
+
: 'The task and this conversation stay linked.'
|
|
166
|
+
}}
|
|
167
|
+
</p>
|
|
168
|
+
<button
|
|
169
|
+
class="rooms-button rooms-button-primary"
|
|
170
|
+
type="button"
|
|
171
|
+
:disabled="!selectedTask || busy"
|
|
172
|
+
@click="createTask"
|
|
173
|
+
>
|
|
174
|
+
{{
|
|
175
|
+
busy
|
|
176
|
+
? 'Opening…'
|
|
177
|
+
: selectedTask?.roomId
|
|
178
|
+
? 'Open existing room'
|
|
179
|
+
: 'Create and open'
|
|
180
|
+
}}<span v-if="!busy" aria-hidden="true">↗</span>
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</section>
|
|
185
|
+
</Transition>
|
|
186
|
+
<div v-if="error" class="rooms-error" role="alert">
|
|
187
|
+
<p>{{ error }}</p>
|
|
188
|
+
<button
|
|
189
|
+
v-if="createdRoom"
|
|
190
|
+
class="rooms-button"
|
|
191
|
+
type="button"
|
|
192
|
+
:disabled="busy"
|
|
193
|
+
@click="emit('retryOpen')"
|
|
194
|
+
>
|
|
195
|
+
Open {{ createdRoom.title }} <span aria-hidden="true">↗</span>
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div class="rooms-toolbar">
|
|
200
|
+
<div class="rooms-filters" aria-label="Room status">
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
:aria-pressed="!showClosed"
|
|
204
|
+
@click="showClosed = false"
|
|
205
|
+
>
|
|
206
|
+
Open <span>{{ rooms.filter((room) => !room.closed).length }}</span>
|
|
207
|
+
</button>
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
:aria-pressed="showClosed"
|
|
211
|
+
@click="showClosed = true"
|
|
212
|
+
>
|
|
213
|
+
Closed <span>{{ rooms.filter((room) => room.closed).length }}</span>
|
|
214
|
+
</button>
|
|
215
|
+
</div>
|
|
216
|
+
<label class="rooms-search"
|
|
217
|
+
><svg viewBox="0 0 24 24" aria-hidden="true">
|
|
218
|
+
<circle cx="10.5" cy="10.5" r="6.5" />
|
|
219
|
+
<path d="m16 16 4 4" /></svg
|
|
220
|
+
><input
|
|
221
|
+
v-model="query"
|
|
222
|
+
type="search"
|
|
223
|
+
aria-label="Search rooms"
|
|
224
|
+
placeholder="Search rooms…"
|
|
225
|
+
/></label>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="rooms-list-caption">
|
|
228
|
+
<span>{{
|
|
229
|
+
showClosed
|
|
230
|
+
? 'Finished conversations & outcomes'
|
|
231
|
+
: 'Conversations in this room'
|
|
232
|
+
}}</span
|
|
233
|
+
><span>Newest first</span>
|
|
234
|
+
</div>
|
|
235
|
+
<div v-if="visibleRooms.length" class="rooms-list">
|
|
236
|
+
<article
|
|
237
|
+
v-for="room in visibleRooms"
|
|
238
|
+
:key="room.id"
|
|
239
|
+
class="rooms-entry"
|
|
240
|
+
:data-expanded="expandedId === room.id"
|
|
241
|
+
>
|
|
242
|
+
<div class="rooms-entry-row">
|
|
243
|
+
<button
|
|
244
|
+
class="rooms-entry-main"
|
|
245
|
+
type="button"
|
|
246
|
+
:aria-label="`${room.closed ? 'View result for' : 'Open'} ${room.title}`"
|
|
247
|
+
@click="
|
|
248
|
+
room.closed ? toggleDetails(room.id) : emit('open', room.id)
|
|
249
|
+
"
|
|
250
|
+
>
|
|
251
|
+
<span
|
|
252
|
+
class="rooms-entry-icon"
|
|
253
|
+
:data-kind="room.kind"
|
|
254
|
+
aria-hidden="true"
|
|
255
|
+
><svg viewBox="0 0 24 24">
|
|
256
|
+
<template v-if="room.closed">
|
|
257
|
+
<path d="m6 12 4 4 8-9" />
|
|
258
|
+
</template>
|
|
259
|
+
<template v-else-if="room.kind === 'branch'">
|
|
260
|
+
<circle cx="7" cy="5" r="2" />
|
|
261
|
+
<circle cx="17" cy="6" r="2" />
|
|
262
|
+
<circle cx="7" cy="19" r="2" />
|
|
263
|
+
<path d="M7 7v10m10-9c0 6-10 2-10 7" />
|
|
264
|
+
</template>
|
|
265
|
+
<template v-else-if="room.kind === 'task'">
|
|
266
|
+
<rect x="5" y="4" width="14" height="17" rx="3" />
|
|
267
|
+
<path d="M9 4V2h6v2M9 12l2 2 4-5" />
|
|
268
|
+
</template>
|
|
269
|
+
<template v-else>
|
|
270
|
+
<path d="M20 11a8 8 0 0 1-8 8H5l-3 2 1-6a8 8 0 1 1 17-4Z" />
|
|
271
|
+
</template></svg
|
|
272
|
+
></span>
|
|
273
|
+
<span class="rooms-entry-copy"
|
|
274
|
+
><span class="rooms-entry-title">{{ room.title }}</span
|
|
275
|
+
><span class="rooms-entry-description"
|
|
276
|
+
><span class="rooms-mobile-kind"
|
|
277
|
+
>{{ roomKindLabel(room) }} · </span
|
|
278
|
+
>{{
|
|
279
|
+
room.description ||
|
|
280
|
+
(room.closed
|
|
281
|
+
? 'No outcome recorded. The conversation is still available.'
|
|
282
|
+
: room.kind === 'branch'
|
|
283
|
+
? 'Conversation for this Git branch'
|
|
284
|
+
: room.kind === 'task'
|
|
285
|
+
? 'A dedicated conversation for this task'
|
|
286
|
+
: 'A separate space for this topic')
|
|
287
|
+
}}</span
|
|
288
|
+
></span
|
|
289
|
+
>
|
|
290
|
+
<span class="rooms-entry-kind">{{ roomKindLabel(room) }}</span>
|
|
291
|
+
<time
|
|
292
|
+
class="rooms-entry-date"
|
|
293
|
+
:datetime="
|
|
294
|
+
(room.closed ? room.closedAt : room.createdAt) || undefined
|
|
295
|
+
"
|
|
296
|
+
>{{
|
|
297
|
+
roomDate(room.closed ? room.closedAt : room.createdAt)
|
|
298
|
+
}}</time
|
|
299
|
+
>
|
|
300
|
+
<span class="rooms-entry-arrow" aria-hidden="true">{{
|
|
301
|
+
room.closed ? '↓' : '↗'
|
|
302
|
+
}}</span>
|
|
303
|
+
</button>
|
|
304
|
+
<button
|
|
305
|
+
:id="`${uid}-toggle-${room.id}`"
|
|
306
|
+
class="rooms-icon-button rooms-details-toggle"
|
|
307
|
+
type="button"
|
|
308
|
+
:aria-label="`${expandedId === room.id ? 'Hide' : 'Show'} details for ${room.title}`"
|
|
309
|
+
:aria-expanded="expandedId === room.id"
|
|
310
|
+
:aria-controls="`${uid}-details-${room.id}`"
|
|
311
|
+
@click="toggleDetails(room.id)"
|
|
312
|
+
>
|
|
313
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
314
|
+
<path
|
|
315
|
+
:d="expandedId === room.id ? 'm7 14 5-5 5 5' : 'm7 10 5 5 5-5'"
|
|
316
|
+
/>
|
|
317
|
+
</svg>
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
<Transition name="rooms-reveal">
|
|
321
|
+
<section
|
|
322
|
+
v-if="expandedId === room.id"
|
|
323
|
+
:id="`${uid}-details-${room.id}`"
|
|
324
|
+
class="rooms-entry-details"
|
|
325
|
+
:aria-label="`${room.title} details`"
|
|
326
|
+
>
|
|
327
|
+
<slot name="details" :room="room"
|
|
328
|
+
><p>{{ room.description }}</p></slot
|
|
329
|
+
>
|
|
330
|
+
</section>
|
|
331
|
+
</Transition>
|
|
332
|
+
</article>
|
|
333
|
+
</div>
|
|
334
|
+
<div v-else class="rooms-empty">
|
|
335
|
+
<div class="rooms-empty-mark" aria-hidden="true">
|
|
336
|
+
<span></span><span></span><span></span>
|
|
337
|
+
</div>
|
|
338
|
+
<h3>
|
|
339
|
+
{{
|
|
340
|
+
query
|
|
341
|
+
? 'No rooms match your search'
|
|
342
|
+
: showClosed
|
|
343
|
+
? 'Outcomes will live here'
|
|
344
|
+
: 'A little room to focus'
|
|
345
|
+
}}
|
|
346
|
+
</h3>
|
|
347
|
+
<p>
|
|
348
|
+
{{
|
|
349
|
+
query
|
|
350
|
+
? 'Try a room name, task, or branch.'
|
|
351
|
+
: showClosed
|
|
352
|
+
? 'Close a room when the work is done. Its conversation and result stay available here.'
|
|
353
|
+
: 'Start a conversation for one task or idea, with space for the right people and agents.'
|
|
354
|
+
}}
|
|
355
|
+
</p>
|
|
356
|
+
<button
|
|
357
|
+
v-if="query"
|
|
358
|
+
class="rooms-button"
|
|
359
|
+
type="button"
|
|
360
|
+
@click="query = ''"
|
|
361
|
+
>
|
|
362
|
+
Clear search
|
|
363
|
+
</button>
|
|
364
|
+
<button
|
|
365
|
+
v-else-if="!showClosed"
|
|
366
|
+
class="rooms-button"
|
|
367
|
+
type="button"
|
|
368
|
+
@click="openComposer"
|
|
369
|
+
>
|
|
370
|
+
Create your first room <span aria-hidden="true">↗</span>
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
<details class="rooms-explainer">
|
|
374
|
+
<summary>How rooms work</summary>
|
|
375
|
+
<div>
|
|
376
|
+
<p>
|
|
377
|
+
<strong>One conversation per task or topic.</strong> People and agents
|
|
378
|
+
can join to work together, then record an outcome when the room
|
|
379
|
+
closes.
|
|
380
|
+
</p>
|
|
381
|
+
<p>
|
|
382
|
+
<strong>Still connected.</strong> New rooms share their final note
|
|
383
|
+
back here by default. Change this in a room’s settings.
|
|
384
|
+
</p>
|
|
385
|
+
<p>
|
|
386
|
+
<strong>Git branches have rooms too.</strong> These follow the branch
|
|
387
|
+
agents are working on. Creating a topic room does not create a Git
|
|
388
|
+
branch or a local checkout.
|
|
389
|
+
</p>
|
|
390
|
+
</div>
|
|
391
|
+
</details>
|
|
392
|
+
</section>
|
|
393
|
+
</template>
|
|
394
|
+
|
|
395
|
+
<script setup lang="ts">
|
|
396
|
+
import { computed, nextTick, ref, useId, watch } from 'vue'
|
|
397
|
+
import {
|
|
398
|
+
filterRooms,
|
|
399
|
+
roomDate,
|
|
400
|
+
roomKindLabel,
|
|
401
|
+
taskStatusLabel,
|
|
402
|
+
type DirectoryRoom,
|
|
403
|
+
type DirectoryTask,
|
|
404
|
+
} from './directory'
|
|
405
|
+
|
|
406
|
+
const props = defineProps<{
|
|
407
|
+
rooms: readonly DirectoryRoom[]
|
|
408
|
+
tasks: readonly DirectoryTask[]
|
|
409
|
+
parentLabel: string
|
|
410
|
+
busy?: boolean
|
|
411
|
+
error?: string | null
|
|
412
|
+
canRefresh?: boolean
|
|
413
|
+
initialTaskId?: string | null
|
|
414
|
+
createdRoom?: { id: string; title: string } | null
|
|
415
|
+
}>()
|
|
416
|
+
const emit = defineEmits<{
|
|
417
|
+
open: [id: string]
|
|
418
|
+
select: [id: string | null]
|
|
419
|
+
createTopic: [title: string]
|
|
420
|
+
createTask: [id: string]
|
|
421
|
+
refresh: []
|
|
422
|
+
retryOpen: []
|
|
423
|
+
}>()
|
|
424
|
+
const uid = useId()
|
|
425
|
+
const composing = ref(false)
|
|
426
|
+
const creationMode = ref<'topic' | 'task'>('topic')
|
|
427
|
+
const topicTitle = ref('')
|
|
428
|
+
const query = ref('')
|
|
429
|
+
const taskQuery = ref('')
|
|
430
|
+
const showClosed = ref(false)
|
|
431
|
+
const expandedId = ref<string | null>(null)
|
|
432
|
+
const selectedTaskId = ref<string | null>(null)
|
|
433
|
+
const newRoomButton = ref<HTMLButtonElement | null>(null)
|
|
434
|
+
const titleInput = ref<HTMLInputElement | null>(null)
|
|
435
|
+
const taskSearchInput = ref<HTMLInputElement | null>(null)
|
|
436
|
+
const visibleRooms = computed(() =>
|
|
437
|
+
filterRooms(props.rooms, showClosed.value, query.value),
|
|
438
|
+
)
|
|
439
|
+
const visibleTasks = computed(() =>
|
|
440
|
+
props.tasks.filter((task) =>
|
|
441
|
+
`${task.title} ${task.description}`
|
|
442
|
+
.toLocaleLowerCase()
|
|
443
|
+
.includes(taskQuery.value.trim().toLocaleLowerCase()),
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
const selectedTask = computed(() =>
|
|
447
|
+
visibleTasks.value.find((task) => task.id === selectedTaskId.value),
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
watch(
|
|
451
|
+
() => props.initialTaskId,
|
|
452
|
+
(id) => {
|
|
453
|
+
if (!id) return
|
|
454
|
+
taskQuery.value = ''
|
|
455
|
+
selectedTaskId.value = id
|
|
456
|
+
creationMode.value = 'task'
|
|
457
|
+
composing.value = true
|
|
458
|
+
},
|
|
459
|
+
{ immediate: true },
|
|
460
|
+
)
|
|
461
|
+
watch(visibleRooms, (rooms) => {
|
|
462
|
+
if (expandedId.value && !rooms.some((room) => room.id === expandedId.value)) {
|
|
463
|
+
expandedId.value = null
|
|
464
|
+
emit('select', null)
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
watch(
|
|
468
|
+
visibleTasks,
|
|
469
|
+
(tasks) => {
|
|
470
|
+
if (
|
|
471
|
+
selectedTaskId.value &&
|
|
472
|
+
!tasks.some((task) => task.id === selectedTaskId.value)
|
|
473
|
+
)
|
|
474
|
+
selectedTaskId.value = null
|
|
475
|
+
},
|
|
476
|
+
{ flush: 'sync' },
|
|
477
|
+
)
|
|
478
|
+
async function openComposer() {
|
|
479
|
+
composing.value = true
|
|
480
|
+
await focusComposer()
|
|
481
|
+
}
|
|
482
|
+
async function focusComposer() {
|
|
483
|
+
await nextTick()
|
|
484
|
+
;(creationMode.value === 'topic'
|
|
485
|
+
? titleInput.value
|
|
486
|
+
: taskSearchInput.value
|
|
487
|
+
)?.focus()
|
|
488
|
+
}
|
|
489
|
+
function toggleComposer() {
|
|
490
|
+
if (composing.value) closeComposer()
|
|
491
|
+
else void openComposer()
|
|
492
|
+
}
|
|
493
|
+
async function closeComposer() {
|
|
494
|
+
if (props.busy) return
|
|
495
|
+
composing.value = false
|
|
496
|
+
await nextTick()
|
|
497
|
+
newRoomButton.value?.focus()
|
|
498
|
+
}
|
|
499
|
+
async function setCreationMode(mode: 'topic' | 'task') {
|
|
500
|
+
creationMode.value = mode
|
|
501
|
+
await focusComposer()
|
|
502
|
+
}
|
|
503
|
+
function toggleDetails(id: string) {
|
|
504
|
+
expandedId.value = expandedId.value === id ? null : id
|
|
505
|
+
emit('select', expandedId.value)
|
|
506
|
+
}
|
|
507
|
+
function createTopic() {
|
|
508
|
+
if (!props.busy && topicTitle.value.trim())
|
|
509
|
+
emit('createTopic', topicTitle.value.trim())
|
|
510
|
+
}
|
|
511
|
+
function createTask() {
|
|
512
|
+
if (props.busy || !selectedTask.value) return
|
|
513
|
+
if (selectedTask.value.roomId) emit('open', selectedTask.value.roomId)
|
|
514
|
+
else emit('createTask', selectedTask.value.id)
|
|
515
|
+
}
|
|
516
|
+
</script>
|
|
517
|
+
|
|
518
|
+
<style src="./rooms-directory.css"></style>
|