@xpufx/paseo-x-comms 0.3.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/LICENSE +21 -0
- package/README.md +149 -0
- package/client/attribution.ts +7 -0
- package/client/conversations.ts +229 -0
- package/client/main.tsx +885 -0
- package/client/peer-label.ts +98 -0
- package/client/peer-status.tsx +201 -0
- package/client/settings-prototype.tsx +395 -0
- package/client/tool-call.ts +115 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/client/via-x-comms.tsx +15 -0
- package/client/x-comms-conversation.tsx +449 -0
- package/client/x-comms-panel.tsx +6 -0
- package/client/x-comms-pill.tsx +193 -0
- package/client/x-comms-timeline.tsx +176 -0
- package/client/x-comms-tool-call.tsx +85 -0
- package/docs/mesh.md +115 -0
- package/mcp/LICENSE +202 -0
- package/mcp/README.md +287 -0
- package/mcp/mcp-config.example.json +8 -0
- package/mcp/package-lock.json +1186 -0
- package/mcp/package.json +43 -0
- package/mcp/paseo-cross-daemon-comms.example.json +5 -0
- package/mcp/paseo-x-comms.bundled.mjs +36964 -0
- package/mcp/paseo-x-comms.mjs +630 -0
- package/mcp/test/fixtures/extensions/block/10-block.mjs +4 -0
- package/mcp/test/fixtures/extensions/broken-hook/10-thrower.mjs +6 -0
- package/mcp/test/fixtures/extensions/broken-hook/20-good.mjs +4 -0
- package/mcp/test/fixtures/extensions/broken-load/10-thrower.mjs +4 -0
- package/mcp/test/fixtures/extensions/broken-load/20-good.mjs +4 -0
- package/mcp/test/fixtures/extensions/custom-tool/10-tool.mjs +12 -0
- package/mcp/test/fixtures/extensions/tool-block/10-block.mjs +8 -0
- package/mcp/test/fixtures/extensions/transform/10-transform.mjs +15 -0
- package/mcp/test/fixtures/fake-paseo.mjs +92 -0
- package/mcp/test/register-ts-hooks.mjs +6 -0
- package/mcp/test/resolve-ts-hooks.mjs +46 -0
- package/package.json +58 -0
- package/paseo-plugin.json +7 -0
- package/server/conversations-snapshot.ts +249 -0
- package/server/handlers.ts +1081 -0
- package/server/injection.ts +152 -0
- package/server/local-send.ts +153 -0
- package/server/mcp-client.ts +47 -0
- package/server/outbox.ts +233 -0
- package/server/peer-channel.ts +151 -0
- package/server/peer-status.ts +233 -0
- package/server/presence.ts +204 -0
- package/server/registry.ts +215 -0
- package/server/relay-status.ts +9 -0
- package/server/server-status.ts +138 -0
- package/server/settings.ts +81 -0
- package/server/snapshot.ts +176 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp/client.ts +349 -0
- package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
- package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
- package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
- package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
- package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/conversations-snapshot.ts +39 -0
- package/shared/envelope.ts +92 -0
- package/shared/outbox.ts +21 -0
- package/shared/registry.ts +366 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- package/shared/version.ts +2 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import type { PluginLogger } from "./logger";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_BEACON_LABEL_PREFIX = "beacon:";
|
|
4
|
+
|
|
5
|
+
export const BEACON_COLORS = [
|
|
6
|
+
"violet",
|
|
7
|
+
"sky",
|
|
8
|
+
"emerald",
|
|
9
|
+
"orange",
|
|
10
|
+
"pink",
|
|
11
|
+
"indigo",
|
|
12
|
+
"teal",
|
|
13
|
+
"red",
|
|
14
|
+
"amber",
|
|
15
|
+
"blue",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type BeaconColor = (typeof BEACON_COLORS)[number];
|
|
19
|
+
|
|
20
|
+
export interface BeaconLabelState {
|
|
21
|
+
name: string;
|
|
22
|
+
color?: string;
|
|
23
|
+
titleSuffix?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
workspaceId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BeaconSetOptions extends BeaconLabelState {
|
|
29
|
+
workspaceId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BeaconBlinkOptions {
|
|
33
|
+
workspaceId?: string;
|
|
34
|
+
a: BeaconLabelState;
|
|
35
|
+
b: BeaconLabelState;
|
|
36
|
+
intervalMs?: number;
|
|
37
|
+
rounds?: number;
|
|
38
|
+
restoreOnDone?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BeaconClearOptions {
|
|
42
|
+
workspaceId?: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
restoreTitle?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WorkspaceTitleHandle {
|
|
48
|
+
setTitle?: (title: string) => unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BeaconDaemonClient {
|
|
52
|
+
setWorkspaceLabel?: (args: unknown) => unknown;
|
|
53
|
+
updateWorkspaceLabel?: (args: unknown) => unknown;
|
|
54
|
+
removeWorkspaceLabel?: (args: unknown) => unknown;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface WorkspaceBeaconOptions {
|
|
59
|
+
workspaceHandle?: WorkspaceTitleHandle | null;
|
|
60
|
+
daemonClient?: BeaconDaemonClient | null;
|
|
61
|
+
labelPrefix?: string;
|
|
62
|
+
baseTitle?: string;
|
|
63
|
+
logger?: PluginLogger;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BeaconSetResult {
|
|
67
|
+
labelApplied: boolean;
|
|
68
|
+
titleApplied: boolean;
|
|
69
|
+
labelName?: string;
|
|
70
|
+
title?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BeaconClearResult {
|
|
74
|
+
labelCleared: boolean;
|
|
75
|
+
titleRestored: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface BeaconBlinkHandle {
|
|
79
|
+
stop: () => void;
|
|
80
|
+
done: Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveBeaconLabelName(
|
|
84
|
+
name: string,
|
|
85
|
+
prefix: string = DEFAULT_BEACON_LABEL_PREFIX,
|
|
86
|
+
): string {
|
|
87
|
+
const trimmed = (name ?? "").trim();
|
|
88
|
+
if (!prefix) return trimmed;
|
|
89
|
+
if (trimmed.toLowerCase().startsWith(prefix.toLowerCase())) return trimmed;
|
|
90
|
+
return `${prefix}${trimmed}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeBeaconColor(color: string | undefined): string | undefined {
|
|
94
|
+
if (!color) return undefined;
|
|
95
|
+
const normalized = color.trim().toLowerCase();
|
|
96
|
+
if ((BEACON_COLORS as readonly string[]).includes(normalized)) return normalized;
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isFunction(value: unknown): value is (...args: never[]) => unknown {
|
|
101
|
+
return typeof value === "function";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function safeInvoke(
|
|
105
|
+
fn: () => unknown,
|
|
106
|
+
logger?: PluginLogger,
|
|
107
|
+
): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
await fn();
|
|
110
|
+
return true;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger?.debug?.(
|
|
113
|
+
`WorkspaceBeacon host call failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
114
|
+
);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toTimerKey(workspaceId: string | undefined): string {
|
|
120
|
+
return workspaceId ?? "__default__";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class WorkspaceBeacon {
|
|
124
|
+
private workspaceHandle?: WorkspaceTitleHandle | null;
|
|
125
|
+
private daemonClient?: BeaconDaemonClient | null;
|
|
126
|
+
private labelPrefix: string;
|
|
127
|
+
private baseTitle?: string;
|
|
128
|
+
private logger?: PluginLogger;
|
|
129
|
+
private originalTitles = new Map<string, string>();
|
|
130
|
+
private blinkTimers = new Map<string, { timer: ReturnType<typeof setInterval>; finish: () => void }>();
|
|
131
|
+
|
|
132
|
+
constructor(options: WorkspaceBeaconOptions = {}) {
|
|
133
|
+
this.workspaceHandle = options.workspaceHandle ?? null;
|
|
134
|
+
this.daemonClient = options.daemonClient ?? null;
|
|
135
|
+
this.labelPrefix = options.labelPrefix ?? DEFAULT_BEACON_LABEL_PREFIX;
|
|
136
|
+
this.baseTitle = options.baseTitle;
|
|
137
|
+
this.logger = options.logger;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setOptions(options: Partial<WorkspaceBeaconOptions>): void {
|
|
141
|
+
if ("workspaceHandle" in options) this.workspaceHandle = options.workspaceHandle ?? null;
|
|
142
|
+
if ("daemonClient" in options) this.daemonClient = options.daemonClient ?? null;
|
|
143
|
+
if (options.labelPrefix !== undefined) this.labelPrefix = options.labelPrefix;
|
|
144
|
+
if (options.baseTitle !== undefined) this.baseTitle = options.baseTitle;
|
|
145
|
+
if (options.logger !== undefined) this.logger = options.logger;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get activeBlinks(): number {
|
|
149
|
+
return this.blinkTimers.size;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async set(options: BeaconSetOptions): Promise<BeaconSetResult> {
|
|
153
|
+
const result: BeaconSetResult = { labelApplied: false, titleApplied: false };
|
|
154
|
+
try {
|
|
155
|
+
const workspaceId = options.workspaceId;
|
|
156
|
+
const labelName = options.name ? resolveBeaconLabelName(options.name, this.labelPrefix) : undefined;
|
|
157
|
+
result.labelName = labelName;
|
|
158
|
+
|
|
159
|
+
if (workspaceId && labelName) {
|
|
160
|
+
result.labelApplied = await this.applyLabel(workspaceId, labelName, options.color);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const title = this.resolveTitle(options);
|
|
164
|
+
if (title !== undefined) {
|
|
165
|
+
result.titleApplied = await this.applyTitle(workspaceId, title);
|
|
166
|
+
if (result.titleApplied) result.title = title;
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
this.logger?.debug?.(
|
|
170
|
+
`WorkspaceBeacon.set failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
blink(options: BeaconBlinkOptions): BeaconBlinkHandle {
|
|
177
|
+
const intervalMs = Math.max(50, options.intervalMs ?? 3000);
|
|
178
|
+
const totalRounds = options.rounds ?? Number.POSITIVE_INFINITY;
|
|
179
|
+
const workspaceId = options.workspaceId ?? options.a.workspaceId ?? options.b.workspaceId;
|
|
180
|
+
const key = toTimerKey(workspaceId);
|
|
181
|
+
this.stopBlink(key);
|
|
182
|
+
let stopped = false;
|
|
183
|
+
let settled = false;
|
|
184
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
185
|
+
let resolveDone!: () => void;
|
|
186
|
+
const done = new Promise<void>((resolve) => {
|
|
187
|
+
resolveDone = resolve;
|
|
188
|
+
});
|
|
189
|
+
const finish = () => {
|
|
190
|
+
if (settled) return;
|
|
191
|
+
settled = true;
|
|
192
|
+
if (timer) clearInterval(timer);
|
|
193
|
+
const current = this.blinkTimers.get(key);
|
|
194
|
+
if (current?.finish === finish) this.blinkTimers.delete(key);
|
|
195
|
+
if (options.restoreOnDone) {
|
|
196
|
+
void this.clear({
|
|
197
|
+
workspaceId,
|
|
198
|
+
name: options.a.name ?? options.b.name,
|
|
199
|
+
restoreTitle: true,
|
|
200
|
+
}).catch((err) =>
|
|
201
|
+
this.logger?.debug?.(
|
|
202
|
+
`WorkspaceBeacon blink restore failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
resolveDone();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const states: BeaconLabelState[] = [options.a, options.b];
|
|
211
|
+
const tick = () => {
|
|
212
|
+
if (stopped) {
|
|
213
|
+
finish();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (count >= totalRounds) {
|
|
217
|
+
finish();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const state = states[count % 2];
|
|
221
|
+
count += 1;
|
|
222
|
+
void this.set({ ...state, workspaceId: state.workspaceId ?? workspaceId ?? "" }).catch((err) =>
|
|
223
|
+
this.logger?.debug?.(
|
|
224
|
+
`WorkspaceBeacon blink tick failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
if (count >= totalRounds) {
|
|
228
|
+
finish();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
let count = 0;
|
|
232
|
+
void this.set({ ...states[0], workspaceId: states[0].workspaceId ?? workspaceId ?? "" }).catch(
|
|
233
|
+
(err) =>
|
|
234
|
+
this.logger?.debug?.(
|
|
235
|
+
`WorkspaceBeacon blink tick failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
count = 1;
|
|
239
|
+
if (count >= totalRounds) {
|
|
240
|
+
finish();
|
|
241
|
+
} else {
|
|
242
|
+
timer = setInterval(tick, intervalMs);
|
|
243
|
+
this.blinkTimers.set(key, { timer, finish });
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
this.logger?.debug?.(
|
|
247
|
+
`WorkspaceBeacon blink setup failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
248
|
+
);
|
|
249
|
+
finish();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
stop: () => {
|
|
254
|
+
stopped = true;
|
|
255
|
+
finish();
|
|
256
|
+
},
|
|
257
|
+
done,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async clear(options: BeaconClearOptions = {}): Promise<BeaconClearResult> {
|
|
262
|
+
const result: BeaconClearResult = { labelCleared: false, titleRestored: false };
|
|
263
|
+
try {
|
|
264
|
+
this.stopBlink(toTimerKey(options.workspaceId));
|
|
265
|
+
if (options.workspaceId && options.name) {
|
|
266
|
+
result.labelCleared = await this.detachLabel(
|
|
267
|
+
options.workspaceId,
|
|
268
|
+
resolveBeaconLabelName(options.name, this.labelPrefix),
|
|
269
|
+
);
|
|
270
|
+
} else if (options.workspaceId && !options.name) {
|
|
271
|
+
result.labelCleared = false;
|
|
272
|
+
}
|
|
273
|
+
if (options.restoreTitle !== false) {
|
|
274
|
+
result.titleRestored = await this.restoreTitle(options.workspaceId);
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
this.logger?.debug?.(
|
|
278
|
+
`WorkspaceBeacon.clear failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
stopAll(): void {
|
|
285
|
+
for (const entry of this.blinkTimers.values()) {
|
|
286
|
+
clearInterval(entry.timer);
|
|
287
|
+
try {
|
|
288
|
+
entry.finish();
|
|
289
|
+
} catch (err) {
|
|
290
|
+
this.logger?.debug?.(
|
|
291
|
+
`WorkspaceBeacon stopAll finish failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
this.blinkTimers.clear();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private resolveTitle(options: BeaconLabelState): string | undefined {
|
|
299
|
+
if (options.title !== undefined) return options.title;
|
|
300
|
+
if (options.titleSuffix === undefined) return undefined;
|
|
301
|
+
const base = this.originalTitles.get(options.workspaceId ?? "") ?? this.baseTitle ?? "";
|
|
302
|
+
return `${base}${options.titleSuffix}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async applyLabel(workspaceId: string, labelName: string, color?: string): Promise<boolean> {
|
|
306
|
+
const client = this.daemonClient;
|
|
307
|
+
if (!client || !isFunction(client.setWorkspaceLabel)) return false;
|
|
308
|
+
const label: Record<string, string> = { name: labelName };
|
|
309
|
+
const normalized = normalizeBeaconColor(color);
|
|
310
|
+
if (normalized) label.color = normalized;
|
|
311
|
+
else if (color) label.color = color;
|
|
312
|
+
return safeInvoke(
|
|
313
|
+
() =>
|
|
314
|
+
(client.setWorkspaceLabel as (args: unknown) => unknown)({
|
|
315
|
+
workspaceId,
|
|
316
|
+
label,
|
|
317
|
+
assigned: true,
|
|
318
|
+
}),
|
|
319
|
+
this.logger,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async detachLabel(workspaceId: string, labelName: string): Promise<boolean> {
|
|
324
|
+
const client = this.daemonClient as BeaconDaemonClient | null | undefined;
|
|
325
|
+
if (!client) return false;
|
|
326
|
+
try {
|
|
327
|
+
if (isFunction(client.setWorkspaceLabel)) {
|
|
328
|
+
return await safeInvoke(
|
|
329
|
+
() =>
|
|
330
|
+
(client.setWorkspaceLabel as (args: unknown) => unknown)({
|
|
331
|
+
workspaceId,
|
|
332
|
+
label: { name: labelName },
|
|
333
|
+
assigned: false,
|
|
334
|
+
}),
|
|
335
|
+
this.logger,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (isFunction(client.removeWorkspaceLabel)) {
|
|
339
|
+
return await safeInvoke(
|
|
340
|
+
() =>
|
|
341
|
+
(client.removeWorkspaceLabel as (args: unknown) => unknown)({
|
|
342
|
+
workspaceId,
|
|
343
|
+
name: labelName,
|
|
344
|
+
}),
|
|
345
|
+
this.logger,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
} catch (err) {
|
|
350
|
+
this.logger?.debug?.(
|
|
351
|
+
`WorkspaceBeacon detachLabel failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
352
|
+
);
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
setOriginalTitle(workspaceId: string, title: string): void {
|
|
358
|
+
this.originalTitles.set(workspaceId, title);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
hasOriginalTitle(workspaceId: string): boolean {
|
|
362
|
+
return this.originalTitles.has(workspaceId);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
getOriginalTitle(workspaceId: string): string | undefined {
|
|
366
|
+
return this.originalTitles.get(workspaceId);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async applyTitle(workspaceId: string | undefined, title: string): Promise<boolean> {
|
|
370
|
+
const handle = this.workspaceHandle;
|
|
371
|
+
if (!handle || !isFunction(handle.setTitle)) return false;
|
|
372
|
+
const key = workspaceId ?? "";
|
|
373
|
+
if (!this.originalTitles.has(key)) {
|
|
374
|
+
if (this.baseTitle !== undefined) {
|
|
375
|
+
this.originalTitles.set(key, this.baseTitle);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return safeInvoke(() => (handle.setTitle as (t: string) => unknown)(title), this.logger);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private async restoreTitle(workspaceId: string | undefined): Promise<boolean> {
|
|
382
|
+
const handle = this.workspaceHandle;
|
|
383
|
+
if (!handle || !isFunction(handle.setTitle)) return false;
|
|
384
|
+
const key = workspaceId ?? "";
|
|
385
|
+
const original = this.originalTitles.get(key) ?? this.baseTitle;
|
|
386
|
+
if (original === undefined) return false;
|
|
387
|
+
const ok = await safeInvoke(
|
|
388
|
+
() => (handle.setTitle as (t: string) => unknown)(original),
|
|
389
|
+
this.logger,
|
|
390
|
+
);
|
|
391
|
+
if (ok) {
|
|
392
|
+
this.originalTitles.delete(key);
|
|
393
|
+
if (this.baseTitle === original) {
|
|
394
|
+
this.baseTitle = undefined;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return ok;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private stopBlink(key: string): void {
|
|
401
|
+
const entry = this.blinkTimers.get(key);
|
|
402
|
+
if (entry) {
|
|
403
|
+
clearInterval(entry.timer);
|
|
404
|
+
this.blinkTimers.delete(key);
|
|
405
|
+
try {
|
|
406
|
+
entry.finish();
|
|
407
|
+
} catch (err) {
|
|
408
|
+
this.logger?.debug?.(
|
|
409
|
+
`WorkspaceBeacon stopBlink finish failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function createWorkspaceBeacon(options: WorkspaceBeaconOptions = {}): WorkspaceBeacon {
|
|
417
|
+
return new WorkspaceBeacon(options);
|
|
418
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared conversations snapshot for cross-plugin readers (top, mcp-tools).
|
|
5
|
+
* Plain data only: no SDK types, so any consumer parses it with plain JSON.
|
|
6
|
+
*
|
|
7
|
+
* Writers: the x-comms plugin server updates it on every send and on every
|
|
8
|
+
* timeline reconcile (receives), and prunes on retract/archive. Readers
|
|
9
|
+
* never probe and never touch timelines; they read the file and render.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const CONVERSATIONS_SNAPSHOT_VERSION = 1 as const;
|
|
13
|
+
|
|
14
|
+
export const XCommsThreadSnapshotSchema = z.object({
|
|
15
|
+
peerAlias: z.string(),
|
|
16
|
+
peerServerId: z.string(),
|
|
17
|
+
peerAgentId: z.string(),
|
|
18
|
+
peerAgentName: z.string().nullable(),
|
|
19
|
+
localAgentIds: z.array(z.string()),
|
|
20
|
+
lastDirection: z.enum(["incoming", "outgoing"]),
|
|
21
|
+
lastTimestamp: z.string(),
|
|
22
|
+
lastReadAt: z.string(),
|
|
23
|
+
lastSeenAt: z.string(),
|
|
24
|
+
unreadCount: z.number().int().nonnegative(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type XCommsThreadSnapshot = z.infer<typeof XCommsThreadSnapshotSchema>;
|
|
28
|
+
|
|
29
|
+
export const XCommsConversationsSnapshotSchema = z.object({
|
|
30
|
+
version: z.literal(CONVERSATIONS_SNAPSHOT_VERSION),
|
|
31
|
+
updatedAt: z.string(),
|
|
32
|
+
threads: z.array(XCommsThreadSnapshotSchema),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type XCommsConversationsSnapshot = z.infer<typeof XCommsConversationsSnapshotSchema>;
|
|
36
|
+
|
|
37
|
+
export function emptyConversationsSnapshot(): XCommsConversationsSnapshot {
|
|
38
|
+
return { version: CONVERSATIONS_SNAPSHOT_VERSION, updatedAt: new Date(0).toISOString(), threads: [] };
|
|
39
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The x-comms wire envelope: a `[x-comms] ` prefixed JSON block stamped on
|
|
5
|
+
* every cross-daemon message. Pure parsing with no UI imports so the
|
|
6
|
+
* conversation derive and its tests run anywhere.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const META_PREFIX = "[x-comms] ";
|
|
10
|
+
|
|
11
|
+
export const EnvelopeSchema = z.object({
|
|
12
|
+
xComms: z.object({
|
|
13
|
+
version: z.number(),
|
|
14
|
+
type: z.string(),
|
|
15
|
+
direction: z.enum(["incoming", "outgoing"]).optional(),
|
|
16
|
+
sender: z.object({
|
|
17
|
+
agentId: z.string().nullable(),
|
|
18
|
+
agentName: z.string().nullable(),
|
|
19
|
+
host: z.string().nullable(),
|
|
20
|
+
daemonServerId: z.string().nullable(),
|
|
21
|
+
cwd: z.string().nullable(),
|
|
22
|
+
}),
|
|
23
|
+
target: z.object({
|
|
24
|
+
daemon: z.string().nullable(),
|
|
25
|
+
agentId: z.string().nullable(),
|
|
26
|
+
}),
|
|
27
|
+
sentAt: z.string(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type CrossDaemonEnvelope = z.infer<typeof EnvelopeSchema>;
|
|
32
|
+
|
|
33
|
+
export type MessageDirection = "incoming" | "outgoing";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Viewer-relative direction. The wire envelope stamps direction "outgoing"
|
|
37
|
+
* from the sender's side, so only a message from self counts as user-sent.
|
|
38
|
+
*/
|
|
39
|
+
export function viewerDirection(env: CrossDaemonEnvelope, viewerAgentId: string): MessageDirection {
|
|
40
|
+
const senderId = env.xComms.sender.agentId;
|
|
41
|
+
return senderId !== null && senderId === viewerAgentId ? "outgoing" : "incoming";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CardSignal {
|
|
45
|
+
direction: MessageDirection;
|
|
46
|
+
userSent: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pure card signal: red is reserved for user-sent messages only. Peer and
|
|
51
|
+
* agent arrivals render neutral. Never derive red from envelope defaults.
|
|
52
|
+
*/
|
|
53
|
+
export function cardSignal(env: CrossDaemonEnvelope, viewerAgentId: string): CardSignal {
|
|
54
|
+
const direction = viewerDirection(env, viewerAgentId);
|
|
55
|
+
return { direction, userSent: direction === "outgoing" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Collapsed preview shows this many lines. Measured overflow (via
|
|
60
|
+
* onTextLayout) decides whether a Show more toggle appears at all.
|
|
61
|
+
*/
|
|
62
|
+
export const COLLAPSED_LINES = 3;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Pure overflow rule for collapsible bodies: only bodies rendering more
|
|
66
|
+
* lines than the collapsed preview get a toggle. Short messages never do.
|
|
67
|
+
*/
|
|
68
|
+
export function isOverflowing(lineCount: number, maxLines: number = COLLAPSED_LINES): boolean {
|
|
69
|
+
return lineCount > maxLines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Splits a message body into its x-comms envelope (if present) and the
|
|
74
|
+
* remaining human-visible text. The envelope is a prefix our server stamps on
|
|
75
|
+
* every x-comms message; its mere presence is the signal we render on.
|
|
76
|
+
*/
|
|
77
|
+
export function parseEnvelope(text: string): { envelope: CrossDaemonEnvelope; body: string } | null {
|
|
78
|
+
if (!text.startsWith(META_PREFIX)) return null;
|
|
79
|
+
const rest = text.slice(META_PREFIX.length).trimStart();
|
|
80
|
+
const sep = rest.indexOf("\n\n");
|
|
81
|
+
const json = sep === -1 ? rest : rest.slice(0, sep);
|
|
82
|
+
const body = sep === -1 ? "" : rest.slice(sep + 2).trim();
|
|
83
|
+
let raw: unknown;
|
|
84
|
+
try {
|
|
85
|
+
raw = JSON.parse(json);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const parsed = EnvelopeSchema.safeParse(raw);
|
|
90
|
+
if (!parsed.success) return null;
|
|
91
|
+
return { envelope: parsed.data, body };
|
|
92
|
+
}
|
package/shared/outbox.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared contract for the outbox expiry notice. The daemon appends one of
|
|
5
|
+
* these to the sender's local timeline when a held message expires; the client
|
|
6
|
+
* registers a renderer for it. Kept out of the wire envelope on purpose: the
|
|
7
|
+
* notice is local to the sender's daemon (see #12 — no wire change).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const OUTBOX_NOTICE_KIND = "x-comms-outbox-notice";
|
|
11
|
+
export const OUTBOX_NOTICE_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
export const OutboxNoticeSchema = z.object({
|
|
14
|
+
daemon: z.string(),
|
|
15
|
+
agentId: z.string(),
|
|
16
|
+
reason: z.string(),
|
|
17
|
+
attempts: z.number(),
|
|
18
|
+
heldForMs: z.number(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type OutboxNotice = z.infer<typeof OutboxNoticeSchema>;
|