@khorsheed/dsh-room 0.1.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.
- package/CHANGELOG.md +9 -0
- package/README.i18n.yaml +6 -0
- package/README.md +48 -0
- package/README.zh.md +48 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +11481 -0
- package/lib/index.js +3231 -0
- package/lib/invariant.js +26 -0
- package/lib/journal-CuRnWk66.js +435 -0
- package/lib/tool.js +350 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +2615 -0
- package/lib/typert.remote-client.d.ts +55 -0
- package/lib/typert.remote-client.js +1711 -0
- package/lib/types/adapter.d.ts +42 -0
- package/lib/types/adapter.js +41 -0
- package/lib/types/agent-setup.d.ts +141 -0
- package/lib/types/agent-setup.js +145 -0
- package/lib/types/client/InviteAgentAction.d.ts +13 -0
- package/lib/types/client/InviteAgentAction.js +45 -0
- package/lib/types/client/InviteDialog.d.ts +90 -0
- package/lib/types/client/InviteDialog.js +216 -0
- package/lib/types/client/MemberCard.d.ts +33 -0
- package/lib/types/client/MemberCard.js +32 -0
- package/lib/types/client/MembersView.d.ts +5 -0
- package/lib/types/client/MembersView.js +168 -0
- package/lib/types/client/RoomComposer.d.ts +21 -0
- package/lib/types/client/RoomComposer.js +211 -0
- package/lib/types/client/RoomDockCapsules.d.ts +64 -0
- package/lib/types/client/RoomDockCapsules.js +277 -0
- package/lib/types/client/RoomEventView.d.ts +11 -0
- package/lib/types/client/RoomEventView.js +17 -0
- package/lib/types/client/RoomModelPicker.d.ts +18 -0
- package/lib/types/client/RoomModelPicker.js +134 -0
- package/lib/types/client/RoomPlanView.d.ts +17 -0
- package/lib/types/client/RoomPlanView.js +88 -0
- package/lib/types/client/RoomQueueStrip.d.ts +26 -0
- package/lib/types/client/RoomQueueStrip.js +28 -0
- package/lib/types/client/RoomRecoveryView.d.ts +11 -0
- package/lib/types/client/RoomRecoveryView.js +38 -0
- package/lib/types/client/RoomRelayView.d.ts +12 -0
- package/lib/types/client/RoomRelayView.js +20 -0
- package/lib/types/client/RoomRunView.d.ts +15 -0
- package/lib/types/client/RoomRunView.js +50 -0
- package/lib/types/client/RoomSpeechView.d.ts +18 -0
- package/lib/types/client/RoomSpeechView.js +62 -0
- package/lib/types/client/RoomStatsLine.d.ts +61 -0
- package/lib/types/client/RoomStatsLine.js +138 -0
- package/lib/types/client/RoomTaskLineView.d.ts +13 -0
- package/lib/types/client/RoomTaskLineView.js +21 -0
- package/lib/types/client/RoomTodoStrip.d.ts +24 -0
- package/lib/types/client/RoomTodoStrip.js +64 -0
- package/lib/types/client/format.d.ts +31 -0
- package/lib/types/client/format.js +48 -0
- package/lib/types/client/icons.d.ts +27 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +39 -0
- package/lib/types/client/index.js +471 -0
- package/lib/types/client/locales.d.ts +422 -0
- package/lib/types/client/locales.js +413 -0
- package/lib/types/client/member-color.d.ts +13 -0
- package/lib/types/client/member-color.js +22 -0
- package/lib/types/client/name-pool.d.ts +17 -0
- package/lib/types/client/name-pool.js +25 -0
- package/lib/types/client/nodes.d.ts +136 -0
- package/lib/types/client/nodes.js +212 -0
- package/lib/types/client/preset-visibility.d.ts +91 -0
- package/lib/types/client/preset-visibility.js +113 -0
- package/lib/types/client/request-ids.d.ts +7 -0
- package/lib/types/client/request-ids.js +21 -0
- package/lib/types/client/room-store.d.ts +137 -0
- package/lib/types/client/room-store.js +244 -0
- package/lib/types/client/slots.d.ts +319 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/deliveries.d.ts +5 -0
- package/lib/types/deliveries.js +20 -0
- package/lib/types/dispatch.d.ts +105 -0
- package/lib/types/dispatch.js +467 -0
- package/lib/types/index.d.ts +301 -0
- package/lib/types/index.js +1200 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +24 -0
- package/lib/types/journal.d.ts +117 -0
- package/lib/types/journal.js +434 -0
- package/lib/types/plan-service.d.ts +34 -0
- package/lib/types/plan-service.js +266 -0
- package/lib/types/plan.d.ts +168 -0
- package/lib/types/plan.js +494 -0
- package/lib/types/tool.d.ts +56 -0
- package/lib/types/tool.js +389 -0
- package/lib/types/types.d.ts +813 -0
- package/lib/types/types.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +131 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import roomRemote from '@khorsheed/dsh-room/remote';
|
|
2
|
+
import { RoomRequestIds } from './request-ids.js';
|
|
3
|
+
import { en, zh } from './locales.js';
|
|
4
|
+
import { InviteAgentAction } from './InviteAgentAction.js';
|
|
5
|
+
import { MembersView } from './MembersView.js';
|
|
6
|
+
import { RoomComposer, selectRoomComposer } from './RoomComposer.js';
|
|
7
|
+
import { RoomSpeechView } from './RoomSpeechView.js';
|
|
8
|
+
import { RoomRunView } from './RoomRunView.js';
|
|
9
|
+
import { RoomEventView } from './RoomEventView.js';
|
|
10
|
+
import { roomEventDefinition, roomRelayDefinition, roomRunDefinition, roomSpeechDefinition, roomTaskLineDefinition } from './nodes.js';
|
|
11
|
+
import { mainSessionId, RoomStore } from './room-store.js';
|
|
12
|
+
import { RoomPresetVisibility, RegistrationToggle } from './preset-visibility.js';
|
|
13
|
+
import { RoomRelayView } from './RoomRelayView.js';
|
|
14
|
+
import { RoomTaskLineView } from './RoomTaskLineView.js';
|
|
15
|
+
/** Dictionary namespace owned by this plugin. */
|
|
16
|
+
const NS = 'room';
|
|
17
|
+
/**
|
|
18
|
+
* Required services: slots, sessions, the remote channel, and the locale
|
|
19
|
+
* service. `remote.room` is deliberately NOT an inject: this plugin both
|
|
20
|
+
* mounts the namespace (through `$mount` below) and consumes it, and the
|
|
21
|
+
* Cordis property proxy only resolves services declared in `inject` or
|
|
22
|
+
* provided by an ancestor fiber — declaring it would deadlock the loader.
|
|
23
|
+
* The event-Definition registry (`uiConversation.events`) is likewise NOT a
|
|
24
|
+
* static inject: the deferred ctx.inject arm keeps the registration
|
|
25
|
+
* independent of ui-conversation's mount order. The native directory picker
|
|
26
|
+
* (uiWorkspace) is probed at gesture time, never injected.
|
|
27
|
+
*/
|
|
28
|
+
export const inject = ['slots', 'sessions', 'remote', 'locale'];
|
|
29
|
+
/**
|
|
30
|
+
* Client plugin body: mount the Remote, start the store, register the
|
|
31
|
+
* dictionaries, and inject the slot entries.
|
|
32
|
+
* @param ctx - client root context.
|
|
33
|
+
* @returns disposer unwinding the mounted Remote namespace.
|
|
34
|
+
*/
|
|
35
|
+
export async function apply(ctx) {
|
|
36
|
+
const disposers = [];
|
|
37
|
+
try {
|
|
38
|
+
disposers.push(await ctx.remote.$mount(roomRemote));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// A Remote already mounted by another composition fails loud at boot; the
|
|
42
|
+
// rest of the plugin still registers (the action would answer an
|
|
43
|
+
// unmounted namespace with a typed RPC error).
|
|
44
|
+
ctx.logger.error(error);
|
|
45
|
+
}
|
|
46
|
+
const remote = ctx.get('remote.room');
|
|
47
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'room: dictionaries');
|
|
48
|
+
const t = ctx.locale.bind(NS);
|
|
49
|
+
const roomStore = new RoomStore(ctx, remote);
|
|
50
|
+
ctx.effect(() => roomStore.start(), 'room: store');
|
|
51
|
+
// The M3' self-hide criterion for room's session chrome (the invite chip
|
|
52
|
+
// and the members tab): the official preset-composition data, with the
|
|
53
|
+
// RoomStore's cached verdict as the actual-room escape.
|
|
54
|
+
const roomChrome = new RoomPresetVisibility(ctx, sessionId => roomStore.isRoomCached(sessionId) === true);
|
|
55
|
+
const requestIds = new RoomRequestIds();
|
|
56
|
+
const submit = async (sessionId, text, targets) => {
|
|
57
|
+
if (remote === undefined)
|
|
58
|
+
return { ok: false, message: t('composer.error.generic') };
|
|
59
|
+
const requestId = requestIds.forInput(sessionId, text, targets);
|
|
60
|
+
const carried = await remote.postMessage({
|
|
61
|
+
sessionId, text, requestId, ...targets === undefined || targets.length === 0 ? {} : { targets },
|
|
62
|
+
});
|
|
63
|
+
if (!carried.ok)
|
|
64
|
+
return { ok: false, message: t('composer.error.generic') };
|
|
65
|
+
const result = carried.value;
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
return result.error.code === 'unknown-targets'
|
|
68
|
+
? { ok: false, message: t('composer.error.unknownTargets', { names: result.error.names.join(' ') }) }
|
|
69
|
+
: { ok: false, message: t('composer.error.generic') };
|
|
70
|
+
}
|
|
71
|
+
requestIds.complete(sessionId, requestId);
|
|
72
|
+
void roomStore.refresh(sessionId);
|
|
73
|
+
return { ok: true };
|
|
74
|
+
};
|
|
75
|
+
// alpha.2 deleted sessions.open; uiWorkspace.openSession is the entry on
|
|
76
|
+
// both lines (0.1.5 navigation.ts:134, alpha.2 navigation.ts:161) and
|
|
77
|
+
// throws synchronously on a target the runtime cannot retain — a torn-down
|
|
78
|
+
// member session degrades to staying on the current view. Probed at
|
|
79
|
+
// gesture time like browseDirectory, never injected.
|
|
80
|
+
const openSession = (sessionId) => {
|
|
81
|
+
const uiWorkspace = ctx.get('uiWorkspace');
|
|
82
|
+
try {
|
|
83
|
+
uiWorkspace?.openSession?.(sessionId);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// An unopenable target leaves the current view in place.
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// The official per-session model directories (ui-model-selection's public
|
|
90
|
+
// client service, augmented onto Context by that plugin): the composer's
|
|
91
|
+
// main-agent model picker resolves the SAME directory the official model
|
|
92
|
+
// seat renders, so its select() writes exactly the official seat's durable
|
|
93
|
+
// per-session selection. Probed once — a host without the plugin gets no
|
|
94
|
+
// picker (duck-type guard, never an inject: absence must not fail the boot).
|
|
95
|
+
const modelDirectories = (() => {
|
|
96
|
+
const candidate = ctx.get('modelDirectories');
|
|
97
|
+
return candidate !== undefined && typeof candidate.directoryFor === 'function'
|
|
98
|
+
? candidate
|
|
99
|
+
: undefined;
|
|
100
|
+
})();
|
|
101
|
+
/** The room session's own directory; undefined degrades to no picker. */
|
|
102
|
+
const modelDirectoryFor = (sessionId) => {
|
|
103
|
+
if (modelDirectories === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
// directoryFor fails loud on an unknown session — a resolving race must
|
|
106
|
+
// never break the composer inject.
|
|
107
|
+
try {
|
|
108
|
+
return modelDirectories.directoryFor(sessionId);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const cancelMember = async (sessionId, member) => {
|
|
115
|
+
if (remote === undefined)
|
|
116
|
+
return;
|
|
117
|
+
const carried = await remote.cancel({ sessionId, name: member });
|
|
118
|
+
if (carried.ok)
|
|
119
|
+
void roomStore.refresh(sessionId);
|
|
120
|
+
};
|
|
121
|
+
/** The relay gate actions (sessionId binds per entry inject). */
|
|
122
|
+
const relayFace = (sessionId) => ({
|
|
123
|
+
roomStore,
|
|
124
|
+
confirmRelay: async (relayId) => {
|
|
125
|
+
if (remote === undefined)
|
|
126
|
+
return;
|
|
127
|
+
const carried = await remote.confirmRelay({ sessionId, relayId });
|
|
128
|
+
if (carried.ok)
|
|
129
|
+
void roomStore.refresh(sessionId);
|
|
130
|
+
},
|
|
131
|
+
dismissRelay: async (relayId) => {
|
|
132
|
+
if (remote === undefined)
|
|
133
|
+
return;
|
|
134
|
+
const carried = await remote.dismissRelay({ sessionId, relayId });
|
|
135
|
+
if (carried.ok)
|
|
136
|
+
void roomStore.refresh(sessionId);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
/** The task-board actions (sessionId binds per entry inject). */
|
|
140
|
+
const tasksFace = (sessionId) => ({
|
|
141
|
+
roomStore,
|
|
142
|
+
addTask: async (member, title, blockedBy) => {
|
|
143
|
+
if (remote === undefined)
|
|
144
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
145
|
+
const carried = await remote.addTask({
|
|
146
|
+
sessionId, member, title, ...blockedBy === undefined ? {} : { blockedBy },
|
|
147
|
+
});
|
|
148
|
+
if (!carried.ok || !carried.value.ok)
|
|
149
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
150
|
+
void roomStore.refresh(sessionId);
|
|
151
|
+
return { ok: true };
|
|
152
|
+
},
|
|
153
|
+
closeTask: async (taskId, status) => {
|
|
154
|
+
if (remote === undefined)
|
|
155
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
156
|
+
const carried = await remote.closeTask({
|
|
157
|
+
sessionId, taskId, ...status === undefined ? {} : { status },
|
|
158
|
+
});
|
|
159
|
+
if (!carried.ok || !carried.value.ok)
|
|
160
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
161
|
+
void roomStore.refresh(sessionId);
|
|
162
|
+
return { ok: true };
|
|
163
|
+
},
|
|
164
|
+
setGoal: async (text) => {
|
|
165
|
+
if (remote === undefined)
|
|
166
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
167
|
+
const carried = await remote.setGoal({ sessionId, text });
|
|
168
|
+
if (!carried.ok || !carried.value.ok)
|
|
169
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
170
|
+
void roomStore.refresh(sessionId);
|
|
171
|
+
return { ok: true };
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
/** Map a structured RoomFailure to the dialog's localized copy. */
|
|
175
|
+
const failureText = (error) => {
|
|
176
|
+
switch (error.code) {
|
|
177
|
+
case 'coordinator-busy': return t('coordinator.busy');
|
|
178
|
+
case 'coordinator-not-ready': return error.message;
|
|
179
|
+
case 'coordinator-conflict': return t('coordinator.conflict');
|
|
180
|
+
case 'active-coordinator': return t('coordinator.active');
|
|
181
|
+
case 'member-cwd-bound': return t('coordinator.cwdBound');
|
|
182
|
+
case 'main-member': return t('coordinator.nativeRequired');
|
|
183
|
+
case 'configuration-owned-by-core': return t('coordinator.configuration');
|
|
184
|
+
case 'duplicate-name': return t('invite.error.duplicate');
|
|
185
|
+
case 'invalid-name': return t('invite.error.invalid');
|
|
186
|
+
case 'local-agent-unavailable': return t('invite.error.unavailable');
|
|
187
|
+
case 'unknown-provider':
|
|
188
|
+
return t('invite.error.unknownProvider', {
|
|
189
|
+
provider: error.provider,
|
|
190
|
+
available: error.available.join(', ') || '—',
|
|
191
|
+
});
|
|
192
|
+
default: return t('invite.error.generic');
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* The invite dialog's share (sessionId binds per entry inject), consumed by
|
|
197
|
+
* both dialog hosts — the members tab and the fresh-room dock's invite
|
|
198
|
+
* capsule. `browseDirectory` is the official wire primitive the native
|
|
199
|
+
* directory-picker flow drives (`workspaces.pickDirectory`): the picker UI
|
|
200
|
+
* itself is bound to ui-workspace's adopt-as-workspace flow holes and cannot
|
|
201
|
+
* serve a pure path pick, so the dialog consumes the primitive directly.
|
|
202
|
+
*/
|
|
203
|
+
const inviteFace = (sessionId) => ({
|
|
204
|
+
renderHarnessModelPicker: ctx.get('localAgentUi')?.renderHarnessModelPicker,
|
|
205
|
+
// The room session's own cwd: the invite dialog's cwd field placeholder
|
|
206
|
+
// (empty = inherit). Read at inject time; a later cwd change refreshes
|
|
207
|
+
// with the next view mount.
|
|
208
|
+
roomCwd: ctx.sessions.list.getSnapshot().byId[sessionId]?.cwd,
|
|
209
|
+
invite: async (values) => {
|
|
210
|
+
if (remote === undefined)
|
|
211
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
212
|
+
const carried = await remote.invite({ sessionId, ...values });
|
|
213
|
+
if (!carried.ok)
|
|
214
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
215
|
+
if (!carried.value.ok)
|
|
216
|
+
return { ok: false, message: failureText(carried.value.error) };
|
|
217
|
+
void roomStore.refresh(sessionId);
|
|
218
|
+
return { ok: true, pendingFirstTask: carried.value.value.pendingFirstTask };
|
|
219
|
+
},
|
|
220
|
+
listProviders: async () => {
|
|
221
|
+
if (remote === undefined)
|
|
222
|
+
return undefined;
|
|
223
|
+
const carried = await remote.listProviders({});
|
|
224
|
+
return carried.ok ? carried.value : undefined;
|
|
225
|
+
},
|
|
226
|
+
browseDirectory: () => {
|
|
227
|
+
// Host 0.1.2 hangs the native picker on ui-workspace's service (the
|
|
228
|
+
// rc-era workspaces.pickDirectory wire primitive moved there,
|
|
229
|
+
// commit be531688f3). Absent service = the picker's cancel value.
|
|
230
|
+
const uiWorkspace = ctx.get('uiWorkspace');
|
|
231
|
+
return uiWorkspace?.pickDirectory() ?? Promise.resolve(null);
|
|
232
|
+
},
|
|
233
|
+
listNames: () => roomStore.getCached(sessionId)?.members.map(member => member.name) ?? [],
|
|
234
|
+
modelSurface: async (harness) => {
|
|
235
|
+
// The localAgentGateway namespace belongs to the local-agent family's
|
|
236
|
+
// client half: a composition without it (or a brokerless harness)
|
|
237
|
+
// answers undefined and the invite dialog keeps a plain text input.
|
|
238
|
+
const gateway = ctx.get('remote.localAgentGateway');
|
|
239
|
+
if (gateway === undefined || typeof gateway['harnessModel'] !== 'function')
|
|
240
|
+
return undefined;
|
|
241
|
+
const harnessModel = gateway['harnessModel'];
|
|
242
|
+
try {
|
|
243
|
+
const carried = await harnessModel(harness);
|
|
244
|
+
return carried.ok && carried.value !== null ? carried.value : undefined;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
roomChrome,
|
|
251
|
+
});
|
|
252
|
+
const membersFace = (sessionId) => ({
|
|
253
|
+
roomStore,
|
|
254
|
+
setCoordinator: async (memberId, expectedRevision) => {
|
|
255
|
+
if (remote === undefined)
|
|
256
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
257
|
+
const carried = await remote.setCoordinator({ sessionId, memberId, expectedRevision });
|
|
258
|
+
if (!carried.ok)
|
|
259
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
260
|
+
if (!carried.value.ok)
|
|
261
|
+
return { ok: false, message: failureText(carried.value.error) };
|
|
262
|
+
await roomStore.refresh(sessionId);
|
|
263
|
+
return { ok: true };
|
|
264
|
+
},
|
|
265
|
+
renderMemberConfiguration: ctx.get('localAgentUi')?.renderMemberConfiguration,
|
|
266
|
+
...inviteFace(sessionId),
|
|
267
|
+
// The localAgentGateway member model surface (the family client half's
|
|
268
|
+
// namespace, probed lazily like the invite dialog's harnessModel read):
|
|
269
|
+
// the member cards' model hints and the edit dialog's model field. A
|
|
270
|
+
// composition without it answers undefined — no hints, no broker call.
|
|
271
|
+
memberModel: async (childSessionId) => {
|
|
272
|
+
const gateway = ctx.get('remote.localAgentGateway');
|
|
273
|
+
if (gateway === undefined || typeof gateway['memberModel'] !== 'function')
|
|
274
|
+
return undefined;
|
|
275
|
+
try {
|
|
276
|
+
const carried = await gateway['memberModel'](childSessionId);
|
|
277
|
+
return carried.ok ? carried.value : undefined;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
setMemberModel: async (childSessionId, model) => {
|
|
284
|
+
const gateway = ctx.get('remote.localAgentGateway');
|
|
285
|
+
if (gateway === undefined || typeof gateway['setMemberModel'] !== 'function')
|
|
286
|
+
return undefined;
|
|
287
|
+
try {
|
|
288
|
+
const carried = await gateway['setMemberModel'](childSessionId, model);
|
|
289
|
+
return carried.ok ? carried.value : undefined;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
modelDirectory: modelDirectoryFor(sessionId),
|
|
296
|
+
openSession,
|
|
297
|
+
removeMember: async (member) => {
|
|
298
|
+
if (remote === undefined)
|
|
299
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
300
|
+
const carried = await remote.removeMember({ sessionId, name: member });
|
|
301
|
+
if (!carried.ok)
|
|
302
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
303
|
+
if (!carried.value.ok)
|
|
304
|
+
return { ok: false, message: failureText(carried.value.error) };
|
|
305
|
+
void roomStore.refresh(sessionId);
|
|
306
|
+
return { ok: true };
|
|
307
|
+
},
|
|
308
|
+
updateMember: async (member, patch) => {
|
|
309
|
+
if (remote === undefined)
|
|
310
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
311
|
+
const carried = await remote.updateMember({
|
|
312
|
+
sessionId,
|
|
313
|
+
name: member,
|
|
314
|
+
...patch.rename === undefined ? {} : { rename: patch.rename },
|
|
315
|
+
...patch.instructions === undefined ? {} : { instructions: patch.instructions },
|
|
316
|
+
...patch.cwd === undefined ? {} : { cwd: patch.cwd },
|
|
317
|
+
...patch.model === undefined ? {} : { model: patch.model },
|
|
318
|
+
});
|
|
319
|
+
if (!carried.ok)
|
|
320
|
+
return { ok: false, message: t('invite.error.generic') };
|
|
321
|
+
if (!carried.value.ok)
|
|
322
|
+
return { ok: false, message: failureText(carried.value.error) };
|
|
323
|
+
void roomStore.refresh(sessionId);
|
|
324
|
+
return { ok: true };
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
// The journal projections: claim the room/* events into chat nodes. Host
|
|
328
|
+
// 0.1.2 folded the event-Definition registry into `uiConversation.events`
|
|
329
|
+
// (commit be531688f3); the deferred arm never fires in a composition
|
|
330
|
+
// without the service, losing only the room chat rows, never the boot.
|
|
331
|
+
ctx.inject(['uiConversation'], (lctx) => {
|
|
332
|
+
lctx.uiConversation.events.register(roomSpeechDefinition);
|
|
333
|
+
lctx.uiConversation.events.register(roomRunDefinition);
|
|
334
|
+
lctx.uiConversation.events.register(roomEventDefinition);
|
|
335
|
+
lctx.uiConversation.events.register(roomRelayDefinition);
|
|
336
|
+
lctx.uiConversation.events.register(roomTaskLineDefinition);
|
|
337
|
+
});
|
|
338
|
+
// The slots are declared by ui-conversation, whose apply order
|
|
339
|
+
// relative to this plugin is unconstrained: register through slots.inject so
|
|
340
|
+
// each entry waits for the declaration instead of crashing the loader entry
|
|
341
|
+
// at boot.
|
|
342
|
+
// The 邀请 agent entry: every session's header carries it (inviting a plain
|
|
343
|
+
// session promotes it into a room host-side). Ordered after the lineage and
|
|
344
|
+
// jobs entries.
|
|
345
|
+
ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
|
|
346
|
+
name: 'conversation.session.header.actions',
|
|
347
|
+
id: 'room-invite-agent',
|
|
348
|
+
order: 30,
|
|
349
|
+
locale: NS,
|
|
350
|
+
inject: (sessionId) => inviteFace(sessionId),
|
|
351
|
+
}, InviteAgentAction));
|
|
352
|
+
// The composer takeover: claim exactly the cached-room sessions, at the
|
|
353
|
+
// ui-subagent precedence (-10). Pending interactions (the approval panel's
|
|
354
|
+
// priority-1 entry) are yielded by the selector itself — election runs
|
|
355
|
+
// ascending, so without the decline this entry would shadow them (the
|
|
356
|
+
// member-channel rule, local-agent 84a2ed0). A cache miss declines — the
|
|
357
|
+
// freshly opened room shows the official bar for the first pull's duration
|
|
358
|
+
// (accepted, see room-store.ts). The injected face carries the capsule
|
|
359
|
+
// actions, the invite share (the fresh-room dock's invite capsule), AND the
|
|
360
|
+
// main-agent turn Stop: the takeover renders the dock
|
|
361
|
+
// surfaces and the Stop button itself, because their seats hide with the
|
|
362
|
+
// official fallback.
|
|
363
|
+
const composerEntry = {
|
|
364
|
+
name: 'conversation.composer',
|
|
365
|
+
priority: -10,
|
|
366
|
+
locale: NS,
|
|
367
|
+
select: (owner) => selectRoomComposer(owner, id => roomStore.isRoomCached(id) === true),
|
|
368
|
+
inject: (sessionId) => ({
|
|
369
|
+
...tasksFace(sessionId),
|
|
370
|
+
...inviteFace(sessionId),
|
|
371
|
+
reconcileDelivery: async (deliveryId, outcome, evidence) => {
|
|
372
|
+
const result = await remote?.reconcileDelivery({ sessionId, deliveryId, outcome, evidence });
|
|
373
|
+
await roomStore.refresh(sessionId);
|
|
374
|
+
return result?.ok && result.value.ok ? { ok: true } : { ok: false, message: result?.ok && !result.value.ok ? result.value.error.code : t('composer.error.generic') };
|
|
375
|
+
},
|
|
376
|
+
planCommand: async (command) => {
|
|
377
|
+
const result = await remote?.planCommand({ sessionId, command });
|
|
378
|
+
await roomStore.refresh(sessionId);
|
|
379
|
+
return result?.ok && result.value.ok ? { ok: true } : { ok: false, message: result?.ok && !result.value.ok ? result.value.message : t('composer.error.generic') };
|
|
380
|
+
},
|
|
381
|
+
openPlanSession: id => openSession(id),
|
|
382
|
+
submit,
|
|
383
|
+
renderMemberConfiguration: ctx.get('localAgentUi')?.renderMemberConfiguration,
|
|
384
|
+
renderMemberInbox: ctx.get('localAgentUi')?.renderMemberInbox,
|
|
385
|
+
stopMember: name => { void remote?.cancel({ sessionId, name }).then(() => roomStore.refresh(sessionId)); },
|
|
386
|
+
modelDirectory: modelDirectoryFor(sessionId),
|
|
387
|
+
// The hidden official bar's Stop: the runtime session face's cancel
|
|
388
|
+
// (the same verb ui-conversation's own Stop injects). A torn-down
|
|
389
|
+
// binding degrades to a no-op.
|
|
390
|
+
stop: () => {
|
|
391
|
+
void ctx.sessions.binding(sessionId)?.session.cancel().catch(() => {
|
|
392
|
+
// Stop failure surfaces via snapshot.promptError; nothing to restore.
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
}),
|
|
396
|
+
};
|
|
397
|
+
let composerDispose;
|
|
398
|
+
ctx.slots.inject('conversation.composer', () => {
|
|
399
|
+
composerDispose = ctx.slots.register(composerEntry, RoomComposer);
|
|
400
|
+
return () => {
|
|
401
|
+
composerDispose?.();
|
|
402
|
+
composerDispose = undefined;
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
// An in-place promotion of an idle session produces no session frames after
|
|
406
|
+
// the store's flip, and the chain elects at render time — re-registering
|
|
407
|
+
// the entry bumps the slot version, re-rendering and re-electing the
|
|
408
|
+
// outlet.
|
|
409
|
+
roomStore.onPromoted = () => {
|
|
410
|
+
if (composerDispose === undefined)
|
|
411
|
+
return;
|
|
412
|
+
composerDispose();
|
|
413
|
+
composerDispose = ctx.slots.register(composerEntry, RoomComposer);
|
|
414
|
+
};
|
|
415
|
+
// Registration-time text (the tab label) reads through the bound translate
|
|
416
|
+
// as a thunk, so it follows the active locale without re-registration.
|
|
417
|
+
// The members tab's REGISTRATION is the hide level (M3'): the tab strip's
|
|
418
|
+
// buttons enumerate `conversation.view` registrations with no per-session
|
|
419
|
+
// predicate, so a hidden tab means no registration — the toggle registers
|
|
420
|
+
// exactly while the criterion passes for the current session and disposes
|
|
421
|
+
// otherwise (the slot's own subscription re-renders the strip, the same
|
|
422
|
+
// re-registration mechanism the composer promotion bump uses).
|
|
423
|
+
const membersToggle = new RegistrationToggle(() => ctx.slots.register({
|
|
424
|
+
name: 'conversation.view',
|
|
425
|
+
id: 'room-members',
|
|
426
|
+
order: 20,
|
|
427
|
+
locale: NS,
|
|
428
|
+
label: () => t('view.members'),
|
|
429
|
+
inject: (sessionId) => membersFace(sessionId),
|
|
430
|
+
}, MembersView), () => roomChrome.show(mainSessionId(ctx.sessions.list.getSnapshot())));
|
|
431
|
+
ctx.slots.inject('conversation.view', () => {
|
|
432
|
+
membersToggle.setReady(true);
|
|
433
|
+
return () => { membersToggle.setReady(false); };
|
|
434
|
+
});
|
|
435
|
+
ctx.effect(() => roomChrome.subscribe(() => { membersToggle.sync(); }), 'room: members tab visibility');
|
|
436
|
+
// The three chat-node renderers, keyed behind the definitions above.
|
|
437
|
+
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
|
|
438
|
+
name: 'conversation.chat.node',
|
|
439
|
+
key: 'room-speech',
|
|
440
|
+
locale: NS,
|
|
441
|
+
inject: () => ({ roomStore, openSession }),
|
|
442
|
+
}, RoomSpeechView));
|
|
443
|
+
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
|
|
444
|
+
name: 'conversation.chat.node',
|
|
445
|
+
key: 'room-run',
|
|
446
|
+
locale: NS,
|
|
447
|
+
inject: (sessionId) => ({
|
|
448
|
+
renderMemberOutput: ctx.get('localAgentUi')?.renderMemberOutput,
|
|
449
|
+
roomStore,
|
|
450
|
+
openSession,
|
|
451
|
+
cancelMember: member => cancelMember(sessionId, member),
|
|
452
|
+
}),
|
|
453
|
+
}, RoomRunView));
|
|
454
|
+
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'room-event', locale: NS }, RoomEventView));
|
|
455
|
+
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
|
|
456
|
+
name: 'conversation.chat.node',
|
|
457
|
+
key: 'room-relay',
|
|
458
|
+
locale: NS,
|
|
459
|
+
inject: (sessionId) => relayFace(sessionId),
|
|
460
|
+
}, RoomRelayView));
|
|
461
|
+
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
|
|
462
|
+
name: 'conversation.chat.node',
|
|
463
|
+
key: 'room-task-line',
|
|
464
|
+
locale: NS,
|
|
465
|
+
inject: () => ({ roomStore }),
|
|
466
|
+
}, RoomTaskLineView));
|
|
467
|
+
return async () => {
|
|
468
|
+
for (const dispose of disposers.reverse())
|
|
469
|
+
await dispose();
|
|
470
|
+
};
|
|
471
|
+
}
|