@oh-my-pi/pi-coding-agent 15.11.7 → 15.11.8
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 +30 -2
- package/dist/cli.js +363 -356
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/collab/crypto.d.ts +12 -0
- package/dist/types/collab/guest.d.ts +21 -0
- package/dist/types/collab/host.d.ts +13 -0
- package/dist/types/collab/protocol.d.ts +100 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +21 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/modes/components/agent-hub.d.ts +13 -0
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/hook-selector.d.ts +4 -6
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/status-line/component.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +9 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/types.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +6 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/package.json +14 -12
- package/scripts/bench-guard.ts +71 -0
- package/src/cli/args.ts +2 -0
- package/src/cli-commands.ts +1 -0
- package/src/collab/crypto.ts +57 -0
- package/src/collab/guest.ts +421 -0
- package/src/collab/host.ts +494 -0
- package/src/collab/protocol.ts +191 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +22 -14
- package/src/config/settings-schema.ts +27 -1
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/main.ts +11 -2
- package/src/modes/components/agent-hub.ts +119 -22
- package/src/modes/components/assistant-message.ts +126 -6
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/hook-selector.ts +4 -5
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/status-line/component.ts +21 -1
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +13 -0
- package/src/modes/components/status-line/types.ts +10 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/controllers/input-controller.ts +72 -6
- package/src/modes/controllers/selector-controller.ts +2 -0
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +12 -4
- package/src/modes/types.ts +8 -0
- package/src/modes/utils/ui-helpers.ts +7 -0
- package/src/sdk.ts +239 -36
- package/src/session/agent-session.ts +17 -0
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +9 -3
- package/src/slash-commands/builtin-registry.ts +210 -0
- package/src/tools/read.ts +38 -5
- package/src/tools/write.ts +13 -42
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side WebSocket wrapper for collab live-session sharing.
|
|
3
|
+
*
|
|
4
|
+
* Connects to a relay room, seals/opens AES-GCM frames, and reconnects with
|
|
5
|
+
* exponential backoff on transient drops. Fatal relay close codes (room gone,
|
|
6
|
+
* host conflict, room full) and decryption failures never reconnect.
|
|
7
|
+
*/
|
|
8
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { open, seal } from "./crypto";
|
|
10
|
+
import type { CollabFrame, RelayControlMessage } from "./protocol";
|
|
11
|
+
import { packEnvelope, unpackEnvelope } from "./protocol";
|
|
12
|
+
|
|
13
|
+
const FATAL_CLOSE_REASONS: Record<number, string> = {
|
|
14
|
+
4001: "room closed",
|
|
15
|
+
4004: "no such room",
|
|
16
|
+
4009: "a host is already connected for this room",
|
|
17
|
+
4029: "room is full",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const BACKOFF_BASE_MS = 1_000;
|
|
21
|
+
const BACKOFF_MAX_MS = 30_000;
|
|
22
|
+
/** Max enveloped frames buffered while a reconnect is pending; overflow is dropped. */
|
|
23
|
+
const MAX_PENDING_SENDS = 256;
|
|
24
|
+
|
|
25
|
+
export interface CollabSocketOptions {
|
|
26
|
+
/** wss://host[:port]/r/<roomId> — no query string. */
|
|
27
|
+
wsUrl: string;
|
|
28
|
+
role: "host" | "guest";
|
|
29
|
+
key: CryptoKey;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class CollabSocket {
|
|
33
|
+
/** Fires after every successful (re)connect. */
|
|
34
|
+
onOpen?: () => void;
|
|
35
|
+
onFrame?: (frame: CollabFrame, fromPeer: number) => void;
|
|
36
|
+
onControl?: (msg: RelayControlMessage) => void;
|
|
37
|
+
/** Fires once per terminal close (intentional, fatal code, or bad key). willReconnect=true for transient drops that will retry. */
|
|
38
|
+
onClose?: (reason: string, willReconnect: boolean) => void;
|
|
39
|
+
|
|
40
|
+
readonly #opts: CollabSocketOptions;
|
|
41
|
+
#ws: WebSocket | null = null;
|
|
42
|
+
#retryTimer: NodeJS.Timeout | undefined;
|
|
43
|
+
#attempt = 0;
|
|
44
|
+
/** Terminal state: intentional close or fatal failure. Cleared by connect(). */
|
|
45
|
+
#closed = false;
|
|
46
|
+
/** Serializes seal() so frames hit the wire in send() order. */
|
|
47
|
+
#sendChain: Promise<void> = Promise.resolve();
|
|
48
|
+
/** Serializes open() so frames are delivered in arrival order. */
|
|
49
|
+
#recvChain: Promise<void> = Promise.resolve();
|
|
50
|
+
/** Envelopes sealed while disconnected, flushed on the next open. */
|
|
51
|
+
#pendingSends: Uint8Array[] = [];
|
|
52
|
+
|
|
53
|
+
constructor(opts: CollabSocketOptions) {
|
|
54
|
+
this.#opts = opts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get isOpen(): boolean {
|
|
58
|
+
return this.#ws?.readyState === WebSocket.OPEN;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
connect(): void {
|
|
62
|
+
if (this.#ws || this.#retryTimer) return;
|
|
63
|
+
this.#closed = false;
|
|
64
|
+
this.#attempt = 0;
|
|
65
|
+
this.#openSocket();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
send(frame: CollabFrame, targetPeer = 0): void {
|
|
69
|
+
this.#sendChain = this.#sendChain
|
|
70
|
+
.then(async () => {
|
|
71
|
+
if (this.#closed) {
|
|
72
|
+
logger.debug("collab: dropping frame, socket closed", { t: frame.t });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const sealed = await seal(this.#opts.key, frame);
|
|
76
|
+
const envelope = packEnvelope(targetPeer, sealed);
|
|
77
|
+
const ws = this.#ws;
|
|
78
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
79
|
+
ws.send(envelope);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (this.#pendingSends.length >= MAX_PENDING_SENDS) {
|
|
83
|
+
logger.debug("collab: dropping frame, reconnect buffer full", { t: frame.t });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.#pendingSends.push(envelope);
|
|
87
|
+
})
|
|
88
|
+
.catch((err: unknown) => {
|
|
89
|
+
logger.debug("collab: send failed", { error: String(err) });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Intentional close: clears any retry timer, suppresses reconnect. A later connect() starts fresh. */
|
|
94
|
+
close(): void {
|
|
95
|
+
const hadActivity = this.#ws !== null || this.#retryTimer !== undefined;
|
|
96
|
+
this.#clearRetry();
|
|
97
|
+
const wasClosed = this.#closed;
|
|
98
|
+
this.#closed = true;
|
|
99
|
+
this.#pendingSends.length = 0;
|
|
100
|
+
const ws = this.#ws;
|
|
101
|
+
this.#ws = null;
|
|
102
|
+
if (ws) {
|
|
103
|
+
try {
|
|
104
|
+
ws.close(1000);
|
|
105
|
+
} catch {
|
|
106
|
+
// already closing/closed
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (hadActivity && !wasClosed) this.onClose?.("closed", false);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#openSocket(): void {
|
|
113
|
+
const ws = new WebSocket(`${this.#opts.wsUrl}?role=${this.#opts.role}`);
|
|
114
|
+
ws.binaryType = "arraybuffer";
|
|
115
|
+
this.#ws = ws;
|
|
116
|
+
ws.onopen = () => {
|
|
117
|
+
if (this.#ws !== ws) return;
|
|
118
|
+
this.#attempt = 0;
|
|
119
|
+
for (const envelope of this.#pendingSends) ws.send(envelope);
|
|
120
|
+
this.#pendingSends.length = 0;
|
|
121
|
+
this.onOpen?.();
|
|
122
|
+
};
|
|
123
|
+
ws.onmessage = (event: MessageEvent) => {
|
|
124
|
+
if (this.#ws !== ws) return;
|
|
125
|
+
this.#handleMessage(ws, event.data);
|
|
126
|
+
};
|
|
127
|
+
ws.onerror = () => {
|
|
128
|
+
// The paired close event carries the actionable state; nothing to do here.
|
|
129
|
+
};
|
|
130
|
+
ws.onclose = (event: CloseEvent) => {
|
|
131
|
+
if (this.#ws !== ws) return;
|
|
132
|
+
this.#ws = null;
|
|
133
|
+
this.#handleClose(event.code, event.reason);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#handleMessage(ws: WebSocket, data: unknown): void {
|
|
138
|
+
if (typeof data === "string") {
|
|
139
|
+
try {
|
|
140
|
+
this.onControl?.(JSON.parse(data) as RelayControlMessage);
|
|
141
|
+
} catch {
|
|
142
|
+
logger.debug("collab: ignoring malformed control message");
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data instanceof Uint8Array ? data : null;
|
|
147
|
+
if (!bytes) return;
|
|
148
|
+
const envelope = unpackEnvelope(bytes);
|
|
149
|
+
if (!envelope) return;
|
|
150
|
+
this.#recvChain = this.#recvChain
|
|
151
|
+
.then(async () => {
|
|
152
|
+
if (this.#ws !== ws) return;
|
|
153
|
+
let frame: CollabFrame;
|
|
154
|
+
try {
|
|
155
|
+
frame = await open(this.#opts.key, envelope.payload);
|
|
156
|
+
} catch {
|
|
157
|
+
this.#failFatal("bad key or corrupted frame");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (this.#ws !== ws) return;
|
|
161
|
+
this.onFrame?.(frame, envelope.peerId);
|
|
162
|
+
})
|
|
163
|
+
.catch((err: unknown) => {
|
|
164
|
+
logger.debug("collab: frame handler failed", { error: String(err) });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#handleClose(code: number, reason: string): void {
|
|
169
|
+
if (this.#closed) return;
|
|
170
|
+
const fatalReason = FATAL_CLOSE_REASONS[code];
|
|
171
|
+
if (fatalReason !== undefined) {
|
|
172
|
+
this.#closed = true;
|
|
173
|
+
this.#pendingSends.length = 0;
|
|
174
|
+
this.onClose?.(fatalReason, false);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.onClose?.(reason || `connection lost (code ${code})`, true);
|
|
178
|
+
this.#scheduleRetry();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Decryption failure: wrong key or corrupted frame. Never reconnect. */
|
|
182
|
+
#failFatal(reason: string): void {
|
|
183
|
+
if (this.#closed) return;
|
|
184
|
+
this.#closed = true;
|
|
185
|
+
this.#clearRetry();
|
|
186
|
+
this.#pendingSends.length = 0;
|
|
187
|
+
const ws = this.#ws;
|
|
188
|
+
this.#ws = null;
|
|
189
|
+
if (ws) {
|
|
190
|
+
try {
|
|
191
|
+
ws.close(1000);
|
|
192
|
+
} catch {
|
|
193
|
+
// already closing/closed
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.onClose?.(reason, false);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#scheduleRetry(): void {
|
|
200
|
+
const base = Math.min(BACKOFF_BASE_MS * 2 ** this.#attempt, BACKOFF_MAX_MS);
|
|
201
|
+
this.#attempt++;
|
|
202
|
+
const delay = base * (0.75 + Math.random() * 0.5);
|
|
203
|
+
this.#retryTimer = setTimeout(() => {
|
|
204
|
+
this.#retryTimer = undefined;
|
|
205
|
+
if (this.#closed) return;
|
|
206
|
+
this.#openSocket();
|
|
207
|
+
}, delay);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#clearRetry(): void {
|
|
211
|
+
if (this.#retryTimer !== undefined) {
|
|
212
|
+
clearTimeout(this.#retryTimer);
|
|
213
|
+
this.#retryTimer = undefined;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join a shared collab session from the CLI: launches the interactive TUI and
|
|
3
|
+
* immediately runs `/join <link>`.
|
|
4
|
+
*/
|
|
5
|
+
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { Args, Command } from "@oh-my-pi/pi-utils/cli";
|
|
7
|
+
import { parseArgs } from "../cli/args";
|
|
8
|
+
import { runRootCommand } from "../main";
|
|
9
|
+
|
|
10
|
+
export default class Join extends Command {
|
|
11
|
+
static description = "Join a shared collab session (same as /join)";
|
|
12
|
+
|
|
13
|
+
static args = {
|
|
14
|
+
link: Args.string({
|
|
15
|
+
description: "Collab link shared by the host (/collab)",
|
|
16
|
+
required: true,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
static examples = [`${APP_NAME} join wss://relay.omp.sh/s/abc123#key`];
|
|
21
|
+
|
|
22
|
+
async run(): Promise<void> {
|
|
23
|
+
const { args } = await this.parse(Join);
|
|
24
|
+
const link = args.link?.trim();
|
|
25
|
+
if (!link) {
|
|
26
|
+
process.stderr.write(`Usage: ${APP_NAME} join <link>\n`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
31
|
+
process.stderr.write(`${APP_NAME} join requires an interactive terminal\n`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const parsed = parseArgs([]);
|
|
36
|
+
parsed.join = link;
|
|
37
|
+
await runRootCommand(parsed, []);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -602,6 +602,7 @@ function getConfiguredProviderOrderFromSettings(): string[] {
|
|
|
602
602
|
export class ModelRegistry {
|
|
603
603
|
#models: Model<Api>[] = [];
|
|
604
604
|
#canonicalIndex: CanonicalModelIndex = { records: [], byId: new Map(), bySelector: new Map() };
|
|
605
|
+
#canonicalIndexDirty: boolean = true;
|
|
605
606
|
#customProviderApiKeys: Map<string, string> = new Map();
|
|
606
607
|
#keylessProviders: Set<string> = new Set();
|
|
607
608
|
#discoverableProviders: DiscoveryProviderConfig[] = [];
|
|
@@ -1519,14 +1520,25 @@ export class ModelRegistry {
|
|
|
1519
1520
|
this.#rebuildPending = true;
|
|
1520
1521
|
return;
|
|
1521
1522
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1523
|
+
// Defer the catalog-wide index build to first read. Boot model
|
|
1524
|
+
// resolution reads it only when enabledModels or a default-role pattern
|
|
1525
|
+
// is configured; the empty interactive launch never reads it pre-paint,
|
|
1526
|
+
// so the ~200ms build over the full catalog moves off the first-paint
|
|
1527
|
+
// critical path.
|
|
1528
|
+
this.#canonicalIndexDirty = true;
|
|
1527
1529
|
this.#rebuildPending = false;
|
|
1528
1530
|
}
|
|
1529
1531
|
|
|
1532
|
+
#ensureCanonicalIndex(): CanonicalModelIndex {
|
|
1533
|
+
if (this.#canonicalIndexDirty) {
|
|
1534
|
+
this.#canonicalIndex = logger.time("buildCanonicalModelIndex", () =>
|
|
1535
|
+
buildCanonicalModelIndex(this.#models, getBundledCanonicalReferenceData(), this.#equivalenceConfig),
|
|
1536
|
+
);
|
|
1537
|
+
this.#canonicalIndexDirty = false;
|
|
1538
|
+
}
|
|
1539
|
+
return this.#canonicalIndex;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1530
1542
|
#suspendRebuild(): void {
|
|
1531
1543
|
this.#rebuildSuspended += 1;
|
|
1532
1544
|
}
|
|
@@ -1537,11 +1549,7 @@ export class ModelRegistry {
|
|
|
1537
1549
|
}
|
|
1538
1550
|
if (this.#rebuildSuspended === 0 && this.#rebuildPending) {
|
|
1539
1551
|
this.#rebuildPending = false;
|
|
1540
|
-
this.#
|
|
1541
|
-
this.#models,
|
|
1542
|
-
getBundledCanonicalReferenceData(),
|
|
1543
|
-
this.#equivalenceConfig,
|
|
1544
|
-
);
|
|
1552
|
+
this.#canonicalIndexDirty = true;
|
|
1545
1553
|
}
|
|
1546
1554
|
}
|
|
1547
1555
|
|
|
@@ -1650,7 +1658,7 @@ export class ModelRegistry {
|
|
|
1650
1658
|
getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
|
|
1651
1659
|
const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
|
|
1652
1660
|
const records: CanonicalModelRecord[] = [];
|
|
1653
|
-
for (const record of this.#
|
|
1661
|
+
for (const record of this.#ensureCanonicalIndex().records) {
|
|
1654
1662
|
const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
1655
1663
|
if (variants.length === 0) {
|
|
1656
1664
|
continue;
|
|
@@ -1676,7 +1684,7 @@ export class ModelRegistry {
|
|
|
1676
1684
|
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
1677
1685
|
const preferences = this.#variantPreferences(candidates);
|
|
1678
1686
|
const selections: CanonicalModelSelection[] = [];
|
|
1679
|
-
for (const record of this.#
|
|
1687
|
+
for (const record of this.#ensureCanonicalIndex().records) {
|
|
1680
1688
|
const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
1681
1689
|
if (variants.length === 0) {
|
|
1682
1690
|
continue;
|
|
@@ -1694,7 +1702,7 @@ export class ModelRegistry {
|
|
|
1694
1702
|
}
|
|
1695
1703
|
|
|
1696
1704
|
getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[] {
|
|
1697
|
-
const record = this.#
|
|
1705
|
+
const record = this.#ensureCanonicalIndex().byId.get(canonicalId.trim().toLowerCase());
|
|
1698
1706
|
if (!record) {
|
|
1699
1707
|
return [];
|
|
1700
1708
|
}
|
|
@@ -1712,7 +1720,7 @@ export class ModelRegistry {
|
|
|
1712
1720
|
}
|
|
1713
1721
|
|
|
1714
1722
|
getCanonicalId(model: Model<Api>): string | undefined {
|
|
1715
|
-
return this.#
|
|
1723
|
+
return this.#ensureCanonicalIndex().bySelector.get(formatCanonicalVariantSelector(model).toLowerCase());
|
|
1716
1724
|
}
|
|
1717
1725
|
|
|
1718
1726
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { SHAPE_VARIANT_NAMES } from "@oh-my-pi/snapcompact";
|
|
3
|
+
import { DEFAULT_RELAY_URL } from "../collab/protocol";
|
|
3
4
|
import { AUTO_THINKING, getConfiguredThinkingLevelMetadata, getThinkingLevelMetadata } from "../thinking";
|
|
4
5
|
import {
|
|
5
6
|
TINY_MODEL_DEVICE_DEFAULT,
|
|
@@ -101,6 +102,7 @@ export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
|
|
|
101
102
|
"Approvals",
|
|
102
103
|
"Notifications",
|
|
103
104
|
"Speech",
|
|
105
|
+
"Collab",
|
|
104
106
|
"Magic Keywords",
|
|
105
107
|
"Startup & Updates",
|
|
106
108
|
"Power (macOS)",
|
|
@@ -147,7 +149,8 @@ export type StatusLineSegmentId =
|
|
|
147
149
|
| "cache_write"
|
|
148
150
|
| "cache_hit"
|
|
149
151
|
| "session_name"
|
|
150
|
-
| "usage"
|
|
152
|
+
| "usage"
|
|
153
|
+
| "collab";
|
|
151
154
|
|
|
152
155
|
/** Submenu choice metadata. */
|
|
153
156
|
export type SubmenuOption<V extends string = string> = {
|
|
@@ -1327,6 +1330,29 @@ export const SETTINGS_SCHEMA = {
|
|
|
1327
1330
|
},
|
|
1328
1331
|
},
|
|
1329
1332
|
|
|
1333
|
+
// Collab
|
|
1334
|
+
"collab.relayUrl": {
|
|
1335
|
+
type: "string",
|
|
1336
|
+
default: DEFAULT_RELAY_URL,
|
|
1337
|
+
ui: {
|
|
1338
|
+
tab: "interaction",
|
|
1339
|
+
group: "Collab",
|
|
1340
|
+
label: "Relay URL",
|
|
1341
|
+
description: "Relay used by /collab (wss://host[:port]; self-host with the omp-collab-relay service)",
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
|
|
1345
|
+
"collab.displayName": {
|
|
1346
|
+
type: "string",
|
|
1347
|
+
default: "",
|
|
1348
|
+
ui: {
|
|
1349
|
+
tab: "interaction",
|
|
1350
|
+
group: "Collab",
|
|
1351
|
+
label: "Display Name",
|
|
1352
|
+
description: "Name shown to other collab participants (default: OS username)",
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
|
|
1330
1356
|
// Speech-to-text
|
|
1331
1357
|
"stt.enabled": {
|
|
1332
1358
|
type: "boolean",
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
|
|
2
1
|
import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
|
|
3
2
|
import { slashCommandCapability } from "../capability/slash-command";
|
|
4
3
|
import { appendInlineArgsFallback, templateUsesInlineArgPlaceholders } from "../config/prompt-templates";
|
|
5
4
|
import type { SlashCommand } from "../discovery";
|
|
6
5
|
import { loadCapability } from "../discovery";
|
|
7
|
-
import {
|
|
8
|
-
BUILTIN_SLASH_COMMAND_DEFS,
|
|
9
|
-
type BuiltinSlashCommand,
|
|
10
|
-
type SubcommandDef,
|
|
11
|
-
} from "../slash-commands/builtin-registry";
|
|
12
6
|
import { EMBEDDED_COMMAND_TEMPLATES } from "../task/commands";
|
|
13
7
|
import { parseCommandArgs, substituteArgs } from "../utils/command-args";
|
|
14
8
|
|
|
@@ -24,97 +18,7 @@ export interface SlashCommandInfo {
|
|
|
24
18
|
path?: string;
|
|
25
19
|
}
|
|
26
20
|
|
|
27
|
-
export type { BuiltinSlashCommand, SubcommandDef } from "../slash-commands/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Build getArgumentCompletions from declarative subcommand definitions.
|
|
31
|
-
* Returns subcommand names filtered by prefix in the dropdown.
|
|
32
|
-
*/
|
|
33
|
-
function buildArgumentCompletions(subcommands: SubcommandDef[]): (prefix: string) => AutocompleteItem[] | null {
|
|
34
|
-
return (argumentPrefix: string) => {
|
|
35
|
-
if (argumentPrefix.includes(" ")) return null; // past the subcommand
|
|
36
|
-
const lower = argumentPrefix.toLowerCase();
|
|
37
|
-
const matches = subcommands
|
|
38
|
-
.filter(s => s.name.startsWith(lower))
|
|
39
|
-
.map(s => ({
|
|
40
|
-
value: `${s.name} `,
|
|
41
|
-
label: s.name,
|
|
42
|
-
description: s.description,
|
|
43
|
-
hint: s.usage,
|
|
44
|
-
}));
|
|
45
|
-
return matches.length > 0 ? matches : null;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Build getInlineHint from declarative subcommand definitions.
|
|
51
|
-
* Shows remaining completion + usage as dim ghost text after cursor.
|
|
52
|
-
*/
|
|
53
|
-
function buildSubcommandInlineHint(subcommands: SubcommandDef[]): (argumentText: string) => string | null {
|
|
54
|
-
return (argumentText: string) => {
|
|
55
|
-
const trimmed = argumentText.trimStart();
|
|
56
|
-
const spaceIndex = trimmed.indexOf(" ");
|
|
57
|
-
|
|
58
|
-
if (spaceIndex === -1) {
|
|
59
|
-
// Still typing subcommand name — show remaining chars + usage
|
|
60
|
-
const prefix = trimmed.toLowerCase();
|
|
61
|
-
if (prefix.length === 0) return null;
|
|
62
|
-
const match = subcommands.find(s => s.name.startsWith(prefix));
|
|
63
|
-
if (!match) return null;
|
|
64
|
-
const remaining = match.name.slice(prefix.length);
|
|
65
|
-
return remaining + (match.usage ? ` ${match.usage}` : "");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Subcommand typed — show remaining usage params
|
|
69
|
-
const subName = trimmed.slice(0, spaceIndex).toLowerCase();
|
|
70
|
-
const afterSub = trimmed.slice(spaceIndex + 1);
|
|
71
|
-
const sub = subcommands.find(s => s.name === subName);
|
|
72
|
-
if (!sub?.usage) return null;
|
|
73
|
-
|
|
74
|
-
if (afterSub.length > 0) {
|
|
75
|
-
const usageParts = sub.usage.split(" ");
|
|
76
|
-
const inputParts = afterSub.trim().split(/\s+/);
|
|
77
|
-
const remaining = usageParts.slice(inputParts.length);
|
|
78
|
-
return remaining.length > 0 ? remaining.join(" ") : null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return sub.usage;
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Build getInlineHint for commands with a simple static hint string.
|
|
87
|
-
* Shows the hint only when no arguments have been typed yet.
|
|
88
|
-
*/
|
|
89
|
-
function buildStaticInlineHint(hint: string): (argumentText: string) => string | null {
|
|
90
|
-
return (argumentText: string) => (argumentText.trim().length === 0 ? hint : null);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Materialized builtin slash commands with completion functions derived from
|
|
95
|
-
* declarative subcommand/hint definitions.
|
|
96
|
-
*/
|
|
97
|
-
export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<
|
|
98
|
-
BuiltinSlashCommand & {
|
|
99
|
-
getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null;
|
|
100
|
-
getInlineHint?: (argumentText: string) => string | null;
|
|
101
|
-
}
|
|
102
|
-
> = BUILTIN_SLASH_COMMAND_DEFS.map(cmd => {
|
|
103
|
-
if (cmd.subcommands) {
|
|
104
|
-
return {
|
|
105
|
-
...cmd,
|
|
106
|
-
getArgumentCompletions: buildArgumentCompletions(cmd.subcommands),
|
|
107
|
-
getInlineHint: buildSubcommandInlineHint(cmd.subcommands),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
if (cmd.inlineHint) {
|
|
111
|
-
return {
|
|
112
|
-
...cmd,
|
|
113
|
-
getInlineHint: buildStaticInlineHint(cmd.inlineHint),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
return cmd;
|
|
117
|
-
});
|
|
21
|
+
export type { BuiltinSlashCommand, SubcommandDef } from "../slash-commands/types";
|
|
118
22
|
|
|
119
23
|
/**
|
|
120
24
|
* Represents a custom slash command loaded from a file
|