@trim21/personal-pi-extensions 0.0.344 → 0.0.347
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/README.md +1 -1
- package/package.json +1 -1
- package/src/talk/core.ts +41 -7
- package/src/talk/index.ts +101 -30
- package/src/talk/registry.ts +2 -0
package/README.md
CHANGED
|
@@ -321,7 +321,7 @@ sqlite 文件路径按优先级取第一个可用值:
|
|
|
321
321
|
```
|
|
322
322
|
/talk-group-join # 无参:自动创建一个新 group(uuid 作为组名)并加入
|
|
323
323
|
/talk-group-join <name> # 加入名为 name 的 group;不存在则创建(名字允许字母/数字/-/_)
|
|
324
|
-
/talk-group-join-last # 加入最近创建的 group(方便新开 session
|
|
324
|
+
/talk-group-join-last # 加入最近创建的 group(方便新开 session 快速归队);支持 --name <alias> 设置显示名
|
|
325
325
|
/talk-group-leave # 离开当前 group(组空了自动删除)
|
|
326
326
|
/talk-group-list # 列出所有 group 及其成员,最新创建的在前
|
|
327
327
|
/talk-group-del <name> # 删除指定 group(成员随之变为未入组)
|
package/package.json
CHANGED
package/src/talk/core.ts
CHANGED
|
@@ -530,12 +530,12 @@ export class TalkCore {
|
|
|
530
530
|
return `Invalid group name '${name}'. Allowed: letters, digits, '-' and '_' (max 64 chars).`;
|
|
531
531
|
}
|
|
532
532
|
if (agentName !== undefined) {
|
|
533
|
-
await this.writeSelf({ name: agentName });
|
|
533
|
+
await this.writeSelf({ name: agentName, alias: agentName });
|
|
534
534
|
}
|
|
535
535
|
const nameNote = agentName === undefined ? "" : ` You are visible as "${agentName}".`;
|
|
536
536
|
const existing = await readGroup(this.storage, name);
|
|
537
537
|
if (existing?.members.includes(self.agentId)) {
|
|
538
|
-
return `Already in group ${name} (${existing.members.length} member(s)).${nameNote}`;
|
|
538
|
+
return `Already in group ${name} (${existing.members.length} member(s)). Members: ${await this.groupMemberNames(existing.members)}.${nameNote}`;
|
|
539
539
|
}
|
|
540
540
|
await this.leaveCurrentGroup();
|
|
541
541
|
if (existing) {
|
|
@@ -544,7 +544,12 @@ export class TalkCore {
|
|
|
544
544
|
members: [...existing.members, self.agentId],
|
|
545
545
|
updatedAt: this.now(),
|
|
546
546
|
});
|
|
547
|
-
return `Joined group ${name} (${
|
|
547
|
+
return `Joined group ${name} (${
|
|
548
|
+
existing.members.length + 1
|
|
549
|
+
} member(s)). Members: ${await this.groupMemberNames([
|
|
550
|
+
...existing.members,
|
|
551
|
+
self.agentId,
|
|
552
|
+
])}. You now see only co-members.${nameNote}`;
|
|
548
553
|
}
|
|
549
554
|
const now = this.now();
|
|
550
555
|
await writeGroup(this.storage, {
|
|
@@ -553,15 +558,18 @@ export class TalkCore {
|
|
|
553
558
|
createdAt: now,
|
|
554
559
|
updatedAt: now,
|
|
555
560
|
});
|
|
556
|
-
return `Created group ${name}.${nameNote} Other agents join it with /talk-group-join ${name}.`;
|
|
561
|
+
return `Created group ${name}. Members: ${await this.groupMemberNames([self.agentId])}.${nameNote} Other agents join it with /talk-group-join ${name}.`;
|
|
557
562
|
}
|
|
558
563
|
|
|
559
|
-
/**
|
|
560
|
-
|
|
564
|
+
/**
|
|
565
|
+
* Join the most recently created group; no-op when already in it. When
|
|
566
|
+
* `agentName` is given, the agent's display name is set to it.
|
|
567
|
+
*/
|
|
568
|
+
async groupJoinLast(agentName?: string): Promise<string> {
|
|
561
569
|
const groups = await listGroups(this.storage);
|
|
562
570
|
if (groups.length === 0) return "No groups. Create one with /talk-group-join.";
|
|
563
571
|
const latest = groups.reduce((a, b) => (b.createdAt > a.createdAt ? b : a));
|
|
564
|
-
return this.groupJoin(latest.id);
|
|
572
|
+
return this.groupJoin(latest.id, agentName);
|
|
565
573
|
}
|
|
566
574
|
|
|
567
575
|
/** Leave the current group; an emptied group is deleted. */
|
|
@@ -618,6 +626,32 @@ export class TalkCore {
|
|
|
618
626
|
return `Groups (${groups.length}):\n${lines.join("\n")}`;
|
|
619
627
|
}
|
|
620
628
|
|
|
629
|
+
/** Human-readable member list: `name (shortId)`, self marked `← you`. */
|
|
630
|
+
private async groupMemberNames(memberIds: string[]): Promise<string> {
|
|
631
|
+
const self = this.requireSelf();
|
|
632
|
+
const records = await listRecords(this.storage);
|
|
633
|
+
const label = (agentId: string): string => {
|
|
634
|
+
const rec = records.find((r) => r.agentId === agentId);
|
|
635
|
+
const id = agentId.length > 8 ? `${agentId.slice(0, 8)}…` : agentId;
|
|
636
|
+
return rec ? `${rec.name} (${id})` : `unknown agent (${id})`;
|
|
637
|
+
};
|
|
638
|
+
return memberIds
|
|
639
|
+
.map((agentId) => (agentId === self.agentId ? `${label(agentId)} ← you` : label(agentId)))
|
|
640
|
+
.join(", ");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Status-bar text for the caller: "alias@group" when an explicit alias was
|
|
645
|
+
* set via `talk-group-join --name`, "@group" otherwise; undefined when the
|
|
646
|
+
* caller is in no group (the adapter clears its status bar).
|
|
647
|
+
*/
|
|
648
|
+
async groupStatus(): Promise<string | undefined> {
|
|
649
|
+
const self = this.requireSelf();
|
|
650
|
+
const group = await groupForAgent(this.storage, self.agentId);
|
|
651
|
+
if (!group) return undefined;
|
|
652
|
+
return self.alias ? `${self.alias}@${group.id}` : `@${group.id}`;
|
|
653
|
+
}
|
|
654
|
+
|
|
621
655
|
async send(to: string, body: string): Promise<string> {
|
|
622
656
|
if (!to) return 'send requires "to".';
|
|
623
657
|
if (!body) return 'send requires "message".';
|
package/src/talk/index.ts
CHANGED
|
@@ -14,7 +14,9 @@ import * as path from "node:path";
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
type ExtensionAPI,
|
|
17
|
+
type ExtensionCommandContext,
|
|
17
18
|
type ExtensionContext,
|
|
19
|
+
type ExtensionUIContext,
|
|
18
20
|
getAgentDir,
|
|
19
21
|
truncateToVisualLines,
|
|
20
22
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -146,12 +148,9 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
146
148
|
events: {
|
|
147
149
|
deliver: deliverToAgent,
|
|
148
150
|
notify(content) {
|
|
149
|
-
// Presence transitions are informational —
|
|
150
|
-
//
|
|
151
|
-
pi.
|
|
152
|
-
{ customType: NOTIFY_TYPE, content, display: true },
|
|
153
|
-
{ deliverAs: "nextTurn" },
|
|
154
|
-
);
|
|
151
|
+
// Presence transitions are informational — show in the TUI only, never
|
|
152
|
+
// in LLM context.
|
|
153
|
+
pi.appendEntry(NOTIFY_TYPE, content);
|
|
155
154
|
},
|
|
156
155
|
},
|
|
157
156
|
});
|
|
@@ -161,6 +160,19 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
161
160
|
return undefined;
|
|
162
161
|
}
|
|
163
162
|
|
|
163
|
+
/** Refresh the talk footer/status-bar text: "alias@group", or "@group" with no explicit alias. */
|
|
164
|
+
function refreshGroupStatus(ui: ExtensionUIContext): void {
|
|
165
|
+
void (async () => {
|
|
166
|
+
if (!self) return;
|
|
167
|
+
try {
|
|
168
|
+
const text = await core.groupStatus();
|
|
169
|
+
ui.setStatus("talk", text && ui.theme.fg("accent", text));
|
|
170
|
+
} catch {
|
|
171
|
+
// status bar is best-effort; never break the agent
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
}
|
|
175
|
+
|
|
164
176
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
165
177
|
|
|
166
178
|
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
@@ -177,7 +189,7 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
177
189
|
lastSeenAt: now,
|
|
178
190
|
status: "idle",
|
|
179
191
|
};
|
|
180
|
-
void core.start(self);
|
|
192
|
+
void core.start(self).then(() => refreshGroupStatus(ctx.ui));
|
|
181
193
|
});
|
|
182
194
|
|
|
183
195
|
pi.on("agent_start", () => core.setWorking());
|
|
@@ -274,21 +286,37 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
274
286
|
|
|
275
287
|
type OkResult<TFlags extends TObject> = Extract<CommandResult<TFlags>, { kind: "ok" }>;
|
|
276
288
|
|
|
277
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* Parse a /talk command; on help/error or init failure the text is sent,
|
|
291
|
+
* otherwise run() produces the listing text. With `sendToContext` the result
|
|
292
|
+
* is injected into the LLM context (sendMessage); otherwise it is appended
|
|
293
|
+
* as a display-only session entry (appendEntry, never sent to the LLM).
|
|
294
|
+
*/
|
|
278
295
|
function handleCommand<TFlags extends TObject>(
|
|
279
296
|
spec: CommandSpec<TFlags>,
|
|
280
297
|
args: string,
|
|
298
|
+
ctx: ExtensionCommandContext,
|
|
281
299
|
run: (parsed: OkResult<TFlags>) => Promise<string> | string,
|
|
300
|
+
options?: { sendToContext?: boolean },
|
|
282
301
|
): Promise<void> {
|
|
302
|
+
const emit = (text: string): void => {
|
|
303
|
+
if (options?.sendToContext) {
|
|
304
|
+
pi.sendMessage({ customType: LIST_TYPE, content: text, display: true });
|
|
305
|
+
} else {
|
|
306
|
+
pi.appendEntry(LIST_TYPE, text);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
283
309
|
const parsed = parseCommand(spec, args);
|
|
284
310
|
if (parsed.kind !== "ok") {
|
|
285
|
-
|
|
311
|
+
emit(parsed.text);
|
|
286
312
|
return Promise.resolve();
|
|
287
313
|
}
|
|
288
314
|
return (async () => {
|
|
289
315
|
const initError = requireInit();
|
|
290
|
-
|
|
291
|
-
|
|
316
|
+
emit(initError ?? (await run(parsed)));
|
|
317
|
+
// Group membership can change under any of these commands; keep the
|
|
318
|
+
// footer/status-bar text in sync.
|
|
319
|
+
refreshGroupStatus(ctx.ui);
|
|
292
320
|
})();
|
|
293
321
|
}
|
|
294
322
|
|
|
@@ -327,9 +355,13 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
327
355
|
|
|
328
356
|
const TALK_GROUP_JOIN_LAST_SPEC = {
|
|
329
357
|
name: "talk-group-join-last",
|
|
330
|
-
usage: "",
|
|
331
|
-
description:
|
|
332
|
-
|
|
358
|
+
usage: "[options]",
|
|
359
|
+
description:
|
|
360
|
+
"Join the most recently created agent group (no-op when already in it); --name <alias> additionally sets this agent's display name",
|
|
361
|
+
flags: Type.Object({
|
|
362
|
+
name: Type.Optional(Type.String({ description: "Set this agent's display name" })),
|
|
363
|
+
}),
|
|
364
|
+
flagMeta: { name: { short: "n", valuePlaceholder: "<alias>" } },
|
|
333
365
|
arity: { max: 0 },
|
|
334
366
|
};
|
|
335
367
|
|
|
@@ -368,13 +400,13 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
368
400
|
|
|
369
401
|
pi.registerCommand("talk", {
|
|
370
402
|
description: TALK_SPEC.description,
|
|
371
|
-
handler: (args) => handleCommand(TALK_SPEC, args, async () => core.list()),
|
|
403
|
+
handler: (args, ctx) => handleCommand(TALK_SPEC, args, ctx, async () => core.list()),
|
|
372
404
|
});
|
|
373
405
|
|
|
374
406
|
pi.registerCommand("talk-dead", {
|
|
375
407
|
description: TALK_DEAD_SPEC.description,
|
|
376
|
-
handler: (args) =>
|
|
377
|
-
handleCommand(TALK_DEAD_SPEC, args, async (parsed) => {
|
|
408
|
+
handler: (args, ctx) =>
|
|
409
|
+
handleCommand(TALK_DEAD_SPEC, args, ctx, async (parsed) => {
|
|
378
410
|
if (parsed.flags.all && parsed.args.length > 0) {
|
|
379
411
|
return "--all cannot be combined with an agent id.\nTry '/talk-dead --help' for usage.";
|
|
380
412
|
}
|
|
@@ -388,39 +420,62 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
388
420
|
|
|
389
421
|
pi.registerCommand("talk-group-join", {
|
|
390
422
|
description: TALK_GROUP_JOIN_SPEC.description,
|
|
391
|
-
handler: (args) =>
|
|
392
|
-
handleCommand(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
423
|
+
handler: (args, ctx) =>
|
|
424
|
+
handleCommand(
|
|
425
|
+
TALK_GROUP_JOIN_SPEC,
|
|
426
|
+
args,
|
|
427
|
+
ctx,
|
|
428
|
+
async (parsed) => {
|
|
429
|
+
const agentName = parsed.flags.name?.trim() || undefined;
|
|
430
|
+
if (agentName !== undefined) explicitName = agentName;
|
|
431
|
+
return core.groupJoin(parsed.args[0], agentName);
|
|
432
|
+
},
|
|
433
|
+
{ sendToContext: true },
|
|
434
|
+
),
|
|
397
435
|
});
|
|
398
436
|
|
|
399
437
|
pi.registerCommand("talk-group-join-last", {
|
|
400
438
|
description: TALK_GROUP_JOIN_LAST_SPEC.description,
|
|
401
|
-
handler: (args) =>
|
|
402
|
-
handleCommand(
|
|
439
|
+
handler: (args, ctx) =>
|
|
440
|
+
handleCommand(
|
|
441
|
+
TALK_GROUP_JOIN_LAST_SPEC,
|
|
442
|
+
args,
|
|
443
|
+
ctx,
|
|
444
|
+
async (parsed) => {
|
|
445
|
+
const agentName = parsed.flags.name?.trim() || undefined;
|
|
446
|
+
if (agentName !== undefined) explicitName = agentName;
|
|
447
|
+
return core.groupJoinLast(agentName);
|
|
448
|
+
},
|
|
449
|
+
{ sendToContext: true },
|
|
450
|
+
),
|
|
403
451
|
});
|
|
404
452
|
|
|
405
453
|
pi.registerCommand("talk-group-leave", {
|
|
406
454
|
description: TALK_GROUP_LEAVE_SPEC.description,
|
|
407
|
-
handler: (args
|
|
455
|
+
handler: (args, ctx) =>
|
|
456
|
+
handleCommand(TALK_GROUP_LEAVE_SPEC, args, ctx, async () => core.groupLeave(), {
|
|
457
|
+
sendToContext: true,
|
|
458
|
+
}),
|
|
408
459
|
});
|
|
409
460
|
|
|
410
461
|
pi.registerCommand("talk-group-list", {
|
|
411
462
|
description: TALK_GROUP_LIST_SPEC.description,
|
|
412
|
-
handler: (args
|
|
463
|
+
handler: (args, ctx) =>
|
|
464
|
+
handleCommand(TALK_GROUP_LIST_SPEC, args, ctx, async () => core.groupList()),
|
|
413
465
|
});
|
|
414
466
|
|
|
415
467
|
pi.registerCommand("talk-group-del", {
|
|
416
468
|
description: TALK_GROUP_DEL_SPEC.description,
|
|
417
|
-
handler: (args) =>
|
|
418
|
-
handleCommand(TALK_GROUP_DEL_SPEC, args, async (parsed) =>
|
|
469
|
+
handler: (args, ctx) =>
|
|
470
|
+
handleCommand(TALK_GROUP_DEL_SPEC, args, ctx, async (parsed) =>
|
|
471
|
+
core.groupDelete(parsed.args[0]),
|
|
472
|
+
),
|
|
419
473
|
});
|
|
420
474
|
|
|
421
475
|
pi.registerCommand("talk-group-clear", {
|
|
422
476
|
description: TALK_GROUP_CLEAR_SPEC.description,
|
|
423
|
-
handler: (args
|
|
477
|
+
handler: (args, ctx) =>
|
|
478
|
+
handleCommand(TALK_GROUP_CLEAR_SPEC, args, ctx, async () => core.groupClear()),
|
|
424
479
|
});
|
|
425
480
|
|
|
426
481
|
// ── Delivery card ──────────────────────────────────────────────────────
|
|
@@ -454,4 +509,20 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
454
509
|
invalidate: (): void => undefined,
|
|
455
510
|
};
|
|
456
511
|
});
|
|
512
|
+
|
|
513
|
+
// ── Display-only entries (appendEntry): shown in chat, persisted, never in LLM context ──
|
|
514
|
+
|
|
515
|
+
for (const type of [LIST_TYPE, NOTIFY_TYPE]) {
|
|
516
|
+
pi.registerEntryRenderer<string>(type, (entry, _options, theme) => {
|
|
517
|
+
const text = typeof entry.data === "string" ? entry.data : "";
|
|
518
|
+
if (!text) return;
|
|
519
|
+
return {
|
|
520
|
+
render: (width: number) =>
|
|
521
|
+
truncateToVisualLines(text, Infinity, width, 1).visualLines.map((line) =>
|
|
522
|
+
theme.bg("customMessageBg", line),
|
|
523
|
+
),
|
|
524
|
+
invalidate: (): void => undefined,
|
|
525
|
+
};
|
|
526
|
+
});
|
|
527
|
+
}
|
|
457
528
|
}
|
package/src/talk/registry.ts
CHANGED
|
@@ -36,6 +36,8 @@ export const AgentRecordSchema = Type.Object({
|
|
|
36
36
|
addr: Type.String(),
|
|
37
37
|
agentId: Type.String(),
|
|
38
38
|
name: Type.String(),
|
|
39
|
+
/** Display name explicitly set via `talk-group-join --name`, distinct from the session name. */
|
|
40
|
+
alias: Type.Optional(Type.String()),
|
|
39
41
|
cwd: Type.String(),
|
|
40
42
|
pid: Type.Number(),
|
|
41
43
|
pidStart: Type.Optional(Type.Number()),
|