@xpufx/paseo-slash 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/LICENSE +21 -0
- package/README.md +59 -0
- package/client/commands.ts +122 -0
- package/client/console.tsx +484 -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/docs/screenshots/slash-autocomplete.jpg +0 -0
- package/docs/screenshots/slash-console.jpg +0 -0
- package/package.json +27 -0
- package/paseo-plugin.json +4 -0
- package/server/orchestrate.ts +81 -0
- package/server/resources.ts +132 -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-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/resources.ts +289 -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,289 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineContract,
|
|
3
|
+
defineSettingsContract,
|
|
4
|
+
} from "paseo-plugin-helper/shared";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
export const SLASH_VERSION = "0.1.0";
|
|
8
|
+
|
|
9
|
+
export const SUGGESTED_PREFIX = "slash-";
|
|
10
|
+
|
|
11
|
+
// The host owns the surface registry and exposes no enumeration to plugins, so
|
|
12
|
+
// this list is maintained by hand from in-repo surface registrations and can
|
|
13
|
+
// drift as plugins add surfaces (issue #186).
|
|
14
|
+
export const KNOWN_OPEN_TARGETS: string[] = ["slash-console", "main", "approvals", "paseo-top-dashboard"];
|
|
15
|
+
|
|
16
|
+
export const COMMAND_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
17
|
+
|
|
18
|
+
export const COMMAND_NAME_HINT =
|
|
19
|
+
"Lowercase letters, digits, and dashes; must start with a letter or digit.";
|
|
20
|
+
|
|
21
|
+
export const SendActionSchema = z.object({
|
|
22
|
+
verb: z.literal("send"),
|
|
23
|
+
template: z.string().min(1).max(8000),
|
|
24
|
+
});
|
|
25
|
+
export type SendAction = z.infer<typeof SendActionSchema>;
|
|
26
|
+
|
|
27
|
+
export const OpenActionSchema = z.object({
|
|
28
|
+
verb: z.literal("open"),
|
|
29
|
+
target: z.string().min(1).max(200),
|
|
30
|
+
});
|
|
31
|
+
export type OpenAction = z.infer<typeof OpenActionSchema>;
|
|
32
|
+
|
|
33
|
+
export const RpcActionSchema = z.object({
|
|
34
|
+
verb: z.literal("rpc"),
|
|
35
|
+
operation: z.string().min(1).max(200),
|
|
36
|
+
params: z.record(z.string(), z.unknown()).default({}),
|
|
37
|
+
});
|
|
38
|
+
export type RpcAction = z.infer<typeof RpcActionSchema>;
|
|
39
|
+
|
|
40
|
+
export const SlashActionSchema = z.discriminatedUnion("verb", [
|
|
41
|
+
SendActionSchema,
|
|
42
|
+
OpenActionSchema,
|
|
43
|
+
RpcActionSchema,
|
|
44
|
+
]);
|
|
45
|
+
export type SlashAction = z.infer<typeof SlashActionSchema>;
|
|
46
|
+
|
|
47
|
+
export const SlashCommandSchema = z.object({
|
|
48
|
+
name: z.string().min(1).max(64).regex(COMMAND_NAME_PATTERN),
|
|
49
|
+
title: z.string().min(1).max(120),
|
|
50
|
+
description: z.string().max(500).default(""),
|
|
51
|
+
enabled: z.boolean().default(true),
|
|
52
|
+
action: SlashActionSchema,
|
|
53
|
+
});
|
|
54
|
+
export type SlashCommand = z.infer<typeof SlashCommandSchema>;
|
|
55
|
+
|
|
56
|
+
export const SlashSettingsSchema = z.object({
|
|
57
|
+
prefix: z.string().max(32).default("slash-"),
|
|
58
|
+
commands: z.array(SlashCommandSchema).default([]),
|
|
59
|
+
});
|
|
60
|
+
export type SlashSettings = z.infer<typeof SlashSettingsSchema>;
|
|
61
|
+
|
|
62
|
+
export const slashSettingsContract = defineSettingsContract({
|
|
63
|
+
name: "slash.settings",
|
|
64
|
+
schema: SlashSettingsSchema,
|
|
65
|
+
description: "S/ash command repository and custom command facilities",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const CommandBundleSchema = z.object({
|
|
69
|
+
bundle: z.literal("slash-commands"),
|
|
70
|
+
version: z.literal(1),
|
|
71
|
+
commands: z.array(SlashCommandSchema).min(1).max(200),
|
|
72
|
+
});
|
|
73
|
+
export type CommandBundle = z.infer<typeof CommandBundleSchema>;
|
|
74
|
+
|
|
75
|
+
export function interpolateTemplate(template: string, args: string): string {
|
|
76
|
+
return template.includes("{args}")
|
|
77
|
+
? template.replaceAll("{args}", args)
|
|
78
|
+
: args
|
|
79
|
+
? `${template} ${args}`
|
|
80
|
+
: template;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function withPrefix(prefix: string, name: string): string {
|
|
84
|
+
return prefix ? `${prefix}${name}` : name;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const SEED_COMMANDS: SlashCommand[] = [
|
|
88
|
+
{
|
|
89
|
+
name: "review",
|
|
90
|
+
title: "Review",
|
|
91
|
+
description: "Send a code-review prompt for the current diff",
|
|
92
|
+
enabled: true,
|
|
93
|
+
action: { verb: "send", template: "Review the current changes: {args}" },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "console",
|
|
97
|
+
title: "Console",
|
|
98
|
+
description: "Open the S/ash output console",
|
|
99
|
+
enabled: true,
|
|
100
|
+
action: { verb: "open", target: "slash-console" },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "ping",
|
|
104
|
+
title: "Ping",
|
|
105
|
+
description: "Safe no-op backend operation proving the rpc verb",
|
|
106
|
+
enabled: true,
|
|
107
|
+
action: { verb: "rpc", operation: "slash.ping", params: {} },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "orchestrate",
|
|
111
|
+
title: "Orchestrate",
|
|
112
|
+
description: "Hand the orchestrator role to this agent via the forgejo hook",
|
|
113
|
+
enabled: true,
|
|
114
|
+
action: { verb: "rpc", operation: "slash.orchestrate", params: {} },
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
export type SlashVerb = SlashAction["verb"];
|
|
119
|
+
|
|
120
|
+
export interface SlashCommandDraft {
|
|
121
|
+
name: string;
|
|
122
|
+
title: string;
|
|
123
|
+
description: string;
|
|
124
|
+
enabled: boolean;
|
|
125
|
+
verb: SlashVerb;
|
|
126
|
+
template: string;
|
|
127
|
+
target: string;
|
|
128
|
+
operation: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type CommandDraftErrorField = "name" | "title" | "description" | "action";
|
|
132
|
+
export type CommandDraftErrors = Partial<Record<CommandDraftErrorField, string>>;
|
|
133
|
+
|
|
134
|
+
export type CommandDraftWarningField = "action";
|
|
135
|
+
export type CommandDraftWarnings = Partial<Record<CommandDraftWarningField, string>>;
|
|
136
|
+
|
|
137
|
+
/** Allowlisted rpc operations and known open-surface ids used for live validation. */
|
|
138
|
+
export interface OperationCatalog {
|
|
139
|
+
rpc?: readonly string[];
|
|
140
|
+
open?: readonly string[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function emptyCommandDraft(): SlashCommandDraft {
|
|
144
|
+
return {
|
|
145
|
+
name: "",
|
|
146
|
+
title: "",
|
|
147
|
+
description: "",
|
|
148
|
+
enabled: true,
|
|
149
|
+
verb: "send",
|
|
150
|
+
template: "",
|
|
151
|
+
target: "",
|
|
152
|
+
operation: "",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function draftFromCommand(command: SlashCommand): SlashCommandDraft {
|
|
157
|
+
const draft: SlashCommandDraft = {
|
|
158
|
+
...emptyCommandDraft(),
|
|
159
|
+
name: command.name,
|
|
160
|
+
title: command.title,
|
|
161
|
+
description: command.description,
|
|
162
|
+
enabled: command.enabled,
|
|
163
|
+
verb: command.action.verb,
|
|
164
|
+
};
|
|
165
|
+
if (command.action.verb === "send") draft.template = command.action.template;
|
|
166
|
+
else if (command.action.verb === "open") draft.target = command.action.target;
|
|
167
|
+
else draft.operation = command.action.operation;
|
|
168
|
+
return draft;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function commandFromDraft(draft: SlashCommandDraft): SlashCommand {
|
|
172
|
+
const base = {
|
|
173
|
+
name: draft.name.trim(),
|
|
174
|
+
title: draft.title.trim(),
|
|
175
|
+
description: draft.description,
|
|
176
|
+
enabled: draft.enabled,
|
|
177
|
+
};
|
|
178
|
+
if (draft.verb === "send") return { ...base, action: { verb: "send", template: draft.template } };
|
|
179
|
+
if (draft.verb === "open") return { ...base, action: { verb: "open", target: draft.target } };
|
|
180
|
+
return { ...base, action: { verb: "rpc", operation: draft.operation, params: {} } };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function validateCommandDraft(
|
|
184
|
+
draft: SlashCommandDraft,
|
|
185
|
+
takenNames: readonly string[] = [],
|
|
186
|
+
catalog: OperationCatalog = {},
|
|
187
|
+
): { errors: CommandDraftErrors; warnings: CommandDraftWarnings; command?: SlashCommand } {
|
|
188
|
+
const parsed = SlashCommandSchema.safeParse(commandFromDraft(draft));
|
|
189
|
+
if (!parsed.success) {
|
|
190
|
+
const errors: CommandDraftErrors = {};
|
|
191
|
+
for (const issue of parsed.error.issues) {
|
|
192
|
+
const key = issue.path[0];
|
|
193
|
+
if (key === "name" || key === "title" || key === "description") {
|
|
194
|
+
errors[key] ??= issue.message;
|
|
195
|
+
} else if (key === "action") {
|
|
196
|
+
errors.action ??= issue.message;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { errors, warnings: {} };
|
|
200
|
+
}
|
|
201
|
+
if (takenNames.includes(parsed.data.name)) {
|
|
202
|
+
return { errors: { name: "A command with this name already exists" }, warnings: {} };
|
|
203
|
+
}
|
|
204
|
+
const warnings: CommandDraftWarnings = {};
|
|
205
|
+
if (draft.verb === "rpc" && catalog.rpc?.length && !catalog.rpc.includes(draft.operation.trim())) {
|
|
206
|
+
return {
|
|
207
|
+
errors: { action: `Unknown RPC operation; allowlisted: ${catalog.rpc.join(", ")}` },
|
|
208
|
+
warnings,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (draft.verb === "open" && catalog.open?.length && !catalog.open.includes(draft.target.trim())) {
|
|
212
|
+
warnings.action = `Unknown surface id; the host does not expose its registry. Known: ${catalog.open.join(", ")}`;
|
|
213
|
+
}
|
|
214
|
+
return { errors: {}, warnings, command: parsed.data };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function upsertCommand(
|
|
218
|
+
commands: readonly SlashCommand[],
|
|
219
|
+
originalName: string | null,
|
|
220
|
+
next: SlashCommand,
|
|
221
|
+
): SlashCommand[] {
|
|
222
|
+
if (originalName === null) return [...commands, next];
|
|
223
|
+
return commands.map((command) => (command.name === originalName ? next : command));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function removeCommandByName(commands: readonly SlashCommand[], name: string): SlashCommand[] {
|
|
227
|
+
return commands.filter((command) => command.name !== name);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function missingCatalogCommands(
|
|
231
|
+
commands: readonly SlashCommand[],
|
|
232
|
+
catalog: readonly SlashCommand[],
|
|
233
|
+
): SlashCommand[] {
|
|
234
|
+
const present = new Set(commands.map((command) => command.name));
|
|
235
|
+
return catalog.filter((command) => !present.has(command.name));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function actionSummary(action: SlashAction): string {
|
|
239
|
+
if (action.verb === "send") return "send";
|
|
240
|
+
if (action.verb === "open") return `open → ${action.target}`;
|
|
241
|
+
return `rpc → ${action.operation}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const listCommandsRpc = defineContract({
|
|
245
|
+
name: "slash.commands.list",
|
|
246
|
+
description: "List effective slash commands with prefix applied",
|
|
247
|
+
input: z.object({}).default({}),
|
|
248
|
+
output: z.object({ commands: z.array(SlashCommandSchema) }),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
export const catalogRpc = defineContract({
|
|
252
|
+
name: "slash.catalog.list",
|
|
253
|
+
description: "List the shipped seed command catalog without touching settings",
|
|
254
|
+
input: z.object({}).default({}),
|
|
255
|
+
output: z.object({ commands: z.array(SlashCommandSchema) }),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
export const operationsListRpc = defineContract({
|
|
259
|
+
name: "slash.operations.list",
|
|
260
|
+
description: "List allowlisted rpc operations and the curated known open-surface ids",
|
|
261
|
+
input: z.object({}).default({}),
|
|
262
|
+
output: z.object({ rpc: z.array(z.string()), open: z.array(z.string()) }),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
export const runCommandRpc = defineContract({
|
|
266
|
+
name: "slash.commands.run",
|
|
267
|
+
description: "Run one slash command by name; send/open resolve client-side, rpc runs here",
|
|
268
|
+
input: z.object({ name: z.string(), args: z.string().default(""), agentId: z.string().optional() }),
|
|
269
|
+
output: z.object({
|
|
270
|
+
verb: z.enum(["send", "open", "rpc"]),
|
|
271
|
+
prompt: z.string().optional(),
|
|
272
|
+
target: z.string().optional(),
|
|
273
|
+
result: z.unknown().optional(),
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
export const exportBundleRpc = defineContract({
|
|
278
|
+
name: "slash.bundle.export",
|
|
279
|
+
description: "Export enabled commands as a shareable bundle document",
|
|
280
|
+
input: z.object({}).default({}),
|
|
281
|
+
output: z.object({ bundle: CommandBundleSchema }),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
export const importBundleRpc = defineContract({
|
|
285
|
+
name: "slash.bundle.import",
|
|
286
|
+
description: "Validate and merge a shared command bundle into settings",
|
|
287
|
+
input: z.object({ bundle: CommandBundleSchema, overwrite: z.boolean().default(false) }),
|
|
288
|
+
output: z.object({ commands: z.array(SlashCommandSchema) }),
|
|
289
|
+
});
|