@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.344",
3
+ "version": "0.0.347",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
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} (${existing.members.length + 1} member(s)). You now see only co-members.${nameNote}`;
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
- /** Join the most recently created group; no-op when already in it. */
560
- async groupJoinLast(): Promise<string> {
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 — queue for the next turn
150
- // rather than steering into a busy agent.
151
- pi.sendMessage(
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
- /** Parse a /talk command; on help/error or init failure the text is sent, otherwise run() produces the listing text. */
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
- pi.sendMessage({ customType: LIST_TYPE, content: parsed.text, display: true });
311
+ emit(parsed.text);
286
312
  return Promise.resolve();
287
313
  }
288
314
  return (async () => {
289
315
  const initError = requireInit();
290
- const text = initError ?? (await run(parsed));
291
- pi.sendMessage({ customType: LIST_TYPE, content: text, display: true });
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: "Join the most recently created agent group (no-op when already in it).",
332
- flags: Type.Object({}),
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(TALK_GROUP_JOIN_SPEC, args, async (parsed) => {
393
- const agentName = parsed.flags.name?.trim() || undefined;
394
- if (agentName !== undefined) explicitName = agentName;
395
- return core.groupJoin(parsed.args[0], agentName);
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(TALK_GROUP_JOIN_LAST_SPEC, args, async () => core.groupJoinLast()),
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) => handleCommand(TALK_GROUP_LEAVE_SPEC, args, async () => core.groupLeave()),
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) => handleCommand(TALK_GROUP_LIST_SPEC, args, async () => core.groupList()),
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) => core.groupDelete(parsed.args[0])),
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) => handleCommand(TALK_GROUP_CLEAR_SPEC, args, async () => core.groupClear()),
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
  }
@@ -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()),