pi-sessions 0.8.0 → 0.9.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.
Files changed (104) hide show
  1. package/README.md +70 -37
  2. package/extensions/pi-sessions.ts +125 -0
  3. package/extensions/session-ask/agent.ts +34 -14
  4. package/extensions/{session-ask.ts → session-ask/install.ts} +73 -90
  5. package/extensions/session-ask/navigate.ts +3 -1
  6. package/extensions/session-ask/presenter.ts +23 -0
  7. package/extensions/session-ask/renderer.ts +63 -0
  8. package/extensions/session-ask/tool-contract.ts +27 -0
  9. package/extensions/session-ask/view-model.ts +18 -0
  10. package/extensions/session-auto-title/generate.ts +18 -18
  11. package/extensions/{session-auto-title.ts → session-auto-title/install.ts} +60 -53
  12. package/extensions/session-auto-title/model.ts +25 -17
  13. package/extensions/session-auto-title/retitle.ts +17 -18
  14. package/extensions/session-auto-title/wizard.ts +12 -11
  15. package/extensions/session-handoff/board-view-model.ts +181 -0
  16. package/extensions/session-handoff/board.ts +663 -0
  17. package/extensions/session-handoff/bootstrap.ts +174 -0
  18. package/extensions/session-handoff/extract.ts +101 -122
  19. package/extensions/session-handoff/install.ts +225 -0
  20. package/extensions/session-handoff/kickoff.ts +92 -0
  21. package/extensions/session-handoff/launch/backend.ts +16 -0
  22. package/extensions/session-handoff/launch/deferred.ts +20 -0
  23. package/extensions/session-handoff/launch/ghostty.ts +99 -0
  24. package/extensions/session-handoff/launch/resolution.ts +57 -0
  25. package/extensions/session-handoff/launch/shell.ts +7 -0
  26. package/extensions/session-handoff/launch/tmux.ts +31 -0
  27. package/extensions/session-handoff/launch-options.ts +61 -0
  28. package/extensions/session-handoff/launch-target.ts +91 -0
  29. package/extensions/session-handoff/metadata.ts +152 -63
  30. package/extensions/session-handoff/model.ts +58 -0
  31. package/extensions/session-handoff/query.ts +0 -1
  32. package/extensions/session-handoff/receipt.ts +96 -0
  33. package/extensions/session-handoff/review.ts +2 -28
  34. package/extensions/session-handoff/spawn.ts +76 -181
  35. package/extensions/session-handoff/strong-modal.ts +13 -0
  36. package/extensions/session-handoff/tool-contract.ts +12 -0
  37. package/extensions/session-handoff/tool-presenter.ts +35 -0
  38. package/extensions/session-handoff/tool-renderer.ts +66 -0
  39. package/extensions/session-handoff/tool-schema.ts +54 -0
  40. package/extensions/session-handoff/tool-view-model.ts +49 -0
  41. package/extensions/session-handoff/tool.ts +206 -0
  42. package/extensions/session-handoff/ui.ts +66 -0
  43. package/extensions/session-hooks/install.ts +75 -0
  44. package/extensions/{session-index.ts → session-index/install.ts} +9 -7
  45. package/extensions/session-messaging/broker/process.ts +21 -28
  46. package/extensions/session-messaging/install.ts +78 -0
  47. package/extensions/session-messaging/pi/cancel-session-presenter.ts +64 -0
  48. package/extensions/session-messaging/pi/incoming-message-presenter.ts +32 -0
  49. package/extensions/session-messaging/pi/incoming-message-view-model.ts +20 -0
  50. package/extensions/session-messaging/pi/incoming-runtime.ts +27 -23
  51. package/extensions/session-messaging/pi/message-contracts.ts +32 -0
  52. package/extensions/session-messaging/pi/renderer.ts +19 -12
  53. package/extensions/session-messaging/pi/send-message-presenter.ts +39 -0
  54. package/extensions/session-messaging/pi/send-message-view-model.ts +54 -0
  55. package/extensions/session-messaging/pi/service.ts +262 -81
  56. package/extensions/session-messaging/pi/tools.ts +184 -40
  57. package/extensions/session-search/extract.ts +60 -14
  58. package/extensions/session-search/hooks.ts +10 -13
  59. package/extensions/session-search/install.ts +373 -0
  60. package/extensions/session-search/renderer.ts +195 -0
  61. package/extensions/session-search/tool-contract.ts +51 -0
  62. package/extensions/shared/composition.ts +20 -0
  63. package/extensions/shared/errors.ts +3 -0
  64. package/extensions/shared/model-resolution.ts +73 -0
  65. package/extensions/shared/model-runtime.ts +33 -0
  66. package/extensions/shared/model.ts +7 -10
  67. package/extensions/shared/rendering/collapsible-text.ts +89 -0
  68. package/extensions/shared/rendering/expandable-content-layout.ts +84 -0
  69. package/extensions/shared/rendering/theme.ts +5 -0
  70. package/extensions/shared/session-broker/client.ts +30 -29
  71. package/extensions/shared/session-broker/protocol.ts +128 -10
  72. package/extensions/shared/session-index/common.ts +5 -5
  73. package/extensions/shared/session-index/lineage.ts +0 -3
  74. package/extensions/shared/session-index/schema.ts +0 -1
  75. package/extensions/shared/session-index/scoring.ts +1 -2
  76. package/extensions/shared/session-index/search.ts +11 -4
  77. package/extensions/shared/session-index/store.ts +4 -9
  78. package/extensions/shared/settings.ts +76 -38
  79. package/extensions/shared/text.ts +5 -0
  80. package/extensions/shared/thinking-levels.ts +22 -0
  81. package/extensions/shared/tmux.ts +205 -0
  82. package/extensions/subagents/cancel.ts +111 -0
  83. package/extensions/subagents/classify.ts +52 -0
  84. package/extensions/subagents/install.ts +344 -0
  85. package/extensions/subagents/launch-target.ts +75 -0
  86. package/extensions/subagents/ledger.ts +217 -0
  87. package/extensions/subagents/reconcile.ts +386 -0
  88. package/extensions/subagents/report-message-presenter.ts +36 -0
  89. package/extensions/subagents/report-message-renderer.ts +21 -0
  90. package/extensions/subagents/report-message-view-model.ts +29 -0
  91. package/extensions/subagents/report.ts +159 -0
  92. package/extensions/subagents/roster.ts +262 -0
  93. package/extensions/subagents/wake.ts +173 -0
  94. package/images/handoff-board-subagents.png +0 -0
  95. package/images/handoff-board-user-sessions.png +0 -0
  96. package/images/handoff-subagent-report.png +0 -0
  97. package/images/session-handoff-tool.png +0 -0
  98. package/package.json +10 -16
  99. package/extensions/session-handoff.ts +0 -741
  100. package/extensions/session-hooks.ts +0 -75
  101. package/extensions/session-messaging/pi/message-view.ts +0 -131
  102. package/extensions/session-messaging.ts +0 -30
  103. package/extensions/session-search.ts +0 -419
  104. package/extensions/shared/session-broker/active.ts +0 -26
@@ -0,0 +1,32 @@
1
+ import type { ExpandableContentPresentation } from "../../shared/rendering/expandable-content-layout.ts";
2
+ import type { RenderTheme } from "../../shared/rendering/theme.ts";
3
+ import type { IncomingMessageViewModel } from "./incoming-message-view-model.ts";
4
+
5
+ export function buildIncomingMessagePresentation(
6
+ model: IncomingMessageViewModel,
7
+ theme: RenderTheme,
8
+ ): ExpandableContentPresentation {
9
+ const source = model.sourceSessionName
10
+ ? `${model.sourceSessionName} (${model.sourceSessionId})`
11
+ : model.sourceSessionId;
12
+ const metadata = [
13
+ model.relation,
14
+ model.requestResponse ? "response requested" : undefined,
15
+ ].filter((item): item is string => Boolean(item));
16
+ const metadataHint = metadata.length > 0 ? ` (${metadata.join(", ")})` : "";
17
+
18
+ return {
19
+ header: `${theme.fg("toolTitle", theme.bold("incoming_message"))} ${theme.fg(
20
+ "muted",
21
+ `from ${source}${metadataHint}`,
22
+ )}`,
23
+ ...(model.body
24
+ ? {
25
+ body: {
26
+ text: theme.fg("toolOutput", model.body.replaceAll("\t", " ")),
27
+ collapsedRows: 3,
28
+ },
29
+ }
30
+ : {}),
31
+ };
32
+ }
@@ -0,0 +1,20 @@
1
+ import { normalizeOptionalText } from "../../shared/text.ts";
2
+ import type { ReceivedMessageEntry } from "./message-contracts.ts";
3
+
4
+ export interface IncomingMessageViewModel {
5
+ sourceSessionId: string;
6
+ sourceSessionName?: string | undefined;
7
+ relation?: string | undefined;
8
+ requestResponse: boolean;
9
+ body: string;
10
+ }
11
+
12
+ export function buildIncomingMessageView(received: ReceivedMessageEntry): IncomingMessageViewModel {
13
+ return {
14
+ sourceSessionId: received.source.sessionId,
15
+ sourceSessionName: normalizeOptionalText(received.source.sessionName),
16
+ relation: received.relation,
17
+ requestResponse: received.requestResponse === true,
18
+ body: received.body,
19
+ };
20
+ }
@@ -1,12 +1,11 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { isSessionStarting, SESSION_STARTING_MESSAGE } from "../../session-handoff/metadata.ts";
2
3
  import { safeParseTypeBoxValue } from "../../shared/typebox.ts";
3
4
  import { RECEIVED_MESSAGE_ENTRY_SCHEMA, type ReceivedMessageEntry } from "./message-contracts.ts";
4
5
 
5
6
  export const MESSAGE_RECEIVED_CUSTOM_TYPE = "pi-sessions.message_received";
6
7
  export const SESSION_MESSAGE_CUSTOM_TYPE = "pi-sessions.session_message";
7
8
 
8
- type DeliveryResult = { delivered: true } | { delivered: false; error: string };
9
-
10
9
  interface IncomingMessageActions {
11
10
  appendEntry(customType: string, data: unknown): void;
12
11
  sendMessage(
@@ -29,28 +28,30 @@ export class IncomingSessionMessageRuntime {
29
28
  appendEntry: pi.appendEntry.bind(pi),
30
29
  sendMessage: pi.sendMessage.bind(pi),
31
30
  };
31
+ }
32
32
 
33
- pi.on("session_start", (_event, ctx) => {
34
- this.context = ctx;
35
- });
36
- pi.on("session_shutdown", () => {
37
- this.context = undefined;
38
- });
33
+ bindContext(ctx: ExtensionContext): void {
34
+ this.context = ctx;
39
35
  }
40
36
 
41
- deliver(received: ReceivedMessageEntry): DeliveryResult {
42
- const ctx = this.context;
43
- if (!ctx) {
44
- return { delivered: false, error: "Target session is not ready." };
45
- }
37
+ clearContext(): void {
38
+ this.context = undefined;
39
+ }
46
40
 
47
- try {
48
- this.actions.appendEntry(MESSAGE_RECEIVED_CUSTOM_TYPE, received);
49
- this.inject(ctx, received);
50
- return { delivered: true };
51
- } catch (error) {
52
- return { delivered: false, error: formatError(error) };
41
+ deliver(received: ReceivedMessageEntry): void {
42
+ const ctx = this.requireContext();
43
+ // A starting session is broker-reachable but has not consumed its handoff
44
+ // kickoff. Injecting here would land the message before the kickoff and
45
+ // break the first-message invariant, so refuse delivery until it kicks off.
46
+ if (isSessionStarting(ctx.sessionManager.getBranch())) {
47
+ throw new Error(SESSION_STARTING_MESSAGE);
53
48
  }
49
+ this.actions.appendEntry(MESSAGE_RECEIVED_CUSTOM_TYPE, received);
50
+ this.inject(ctx, received);
51
+ }
52
+
53
+ cancel(): void {
54
+ this.requireContext().abort();
54
55
  }
55
56
 
56
57
  replayPending(ctx: ExtensionContext): void {
@@ -84,6 +85,13 @@ export class IncomingSessionMessageRuntime {
84
85
  }
85
86
  }
86
87
 
88
+ private requireContext(): ExtensionContext {
89
+ if (!this.context) {
90
+ throw new Error("Target session is not ready.");
91
+ }
92
+ return this.context;
93
+ }
94
+
87
95
  private inject(ctx: ExtensionContext, received: ReceivedMessageEntry): void {
88
96
  const deliveryOptions = ctx.isIdle()
89
97
  ? { triggerTurn: true as const }
@@ -108,7 +116,3 @@ function formatIncomingMessageForModel(received: ReceivedMessageEntry): string {
108
116
  const responseLine = received.requestResponse ? "\n\nResponse requested." : "";
109
117
  return `Incoming message from session${titlePart} (session: ${received.source.sessionId}${relationPart}):\n\n${received.body}${responseLine}`;
110
118
  }
111
-
112
- function formatError(error: unknown): string {
113
- return error instanceof Error && error.message.trim() ? error.message : String(error);
114
- }
@@ -19,6 +19,35 @@ export const SEND_MESSAGE_PARAMS = Type.Object({
19
19
  ),
20
20
  });
21
21
 
22
+ export const SEND_MESSAGE_TOOL_DETAILS_SCHEMA = Type.Object({
23
+ delivered: Type.Literal(true),
24
+ messageId: Type.String(),
25
+ target: RECEIVED_MESSAGE_ENDPOINT_SCHEMA,
26
+ relation: Type.Optional(Type.String()),
27
+ });
28
+
29
+ export const CANCEL_SESSION_PARAMS = Type.Object({
30
+ session: Type.String({ description: "Bare UUID for the live session to cancel." }),
31
+ });
32
+
33
+ export const CANCEL_SESSION_TOOL_DETAILS_SCHEMA = Type.Union([
34
+ Type.Object({
35
+ kind: Type.Literal("transport"),
36
+ delivered: Type.Literal(true),
37
+ cancelId: Type.String(),
38
+ target: RECEIVED_MESSAGE_ENDPOINT_SCHEMA,
39
+ relation: Type.Optional(Type.String()),
40
+ }),
41
+ Type.Object({
42
+ kind: Type.Literal("managed"),
43
+ accepted: Type.Literal(true),
44
+ target: RECEIVED_MESSAGE_ENDPOINT_SCHEMA,
45
+ state: Type.String(),
46
+ cancelId: Type.Optional(Type.String()),
47
+ relation: Type.Optional(Type.String()),
48
+ }),
49
+ ]);
50
+
22
51
  export const RECEIVED_MESSAGE_ENTRY_SCHEMA = Type.Object({
23
52
  messageId: Type.String(),
24
53
  source: RECEIVED_MESSAGE_ENDPOINT_SCHEMA,
@@ -32,5 +61,8 @@ export const RECEIVED_MESSAGE_ENTRY_SCHEMA = Type.Object({
32
61
  });
33
62
 
34
63
  export type SendMessageParams = Static<typeof SEND_MESSAGE_PARAMS>;
64
+ export type SendMessageToolDetails = Static<typeof SEND_MESSAGE_TOOL_DETAILS_SCHEMA>;
65
+ export type CancelSessionParams = Static<typeof CANCEL_SESSION_PARAMS>;
66
+ export type CancelSessionToolDetails = Static<typeof CANCEL_SESSION_TOOL_DETAILS_SCHEMA>;
35
67
  export type ReceivedMessageEndpoint = Static<typeof RECEIVED_MESSAGE_ENDPOINT_SCHEMA>;
36
68
  export type ReceivedMessageEntry = Static<typeof RECEIVED_MESSAGE_ENTRY_SCHEMA>;
@@ -1,16 +1,23 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type { MessageRenderer } from "@earendil-works/pi-coding-agent";
2
+ import { Box } from "@earendil-works/pi-tui";
3
+ import { ExpandableContentLayout } from "../../shared/rendering/expandable-content-layout.ts";
2
4
  import { safeParseTypeBoxValue } from "../../shared/typebox.ts";
3
- import { SESSION_MESSAGE_CUSTOM_TYPE } from "./incoming-runtime.ts";
5
+ import { buildIncomingMessagePresentation } from "./incoming-message-presenter.ts";
6
+ import { buildIncomingMessageView } from "./incoming-message-view-model.ts";
4
7
  import { RECEIVED_MESSAGE_ENTRY_SCHEMA } from "./message-contracts.ts";
5
- import { createReceivedSessionMessageComponent } from "./message-view.ts";
6
8
 
7
- export function registerSessionMessagingRenderer(pi: ExtensionAPI): void {
8
- pi.registerMessageRenderer(SESSION_MESSAGE_CUSTOM_TYPE, (message, options, theme) => {
9
- const details = safeParseTypeBoxValue(RECEIVED_MESSAGE_ENTRY_SCHEMA, message.details);
10
- if (!details) {
11
- return undefined;
12
- }
9
+ export const renderIncomingSessionMessage: MessageRenderer = (message, options, theme) => {
10
+ const details = safeParseTypeBoxValue(RECEIVED_MESSAGE_ENTRY_SCHEMA, message.details);
11
+ if (!details) {
12
+ return undefined;
13
+ }
13
14
 
14
- return createReceivedSessionMessageComponent(details, options.expanded, theme);
15
- });
16
- }
15
+ const layout = new ExpandableContentLayout(theme);
16
+ layout.update(
17
+ buildIncomingMessagePresentation(buildIncomingMessageView(details), theme),
18
+ options.expanded,
19
+ );
20
+ const box = new Box(1, 1, (text) => theme.bg("toolSuccessBg", text));
21
+ box.addChild(layout);
22
+ return box;
23
+ };
@@ -0,0 +1,39 @@
1
+ import type { ExpandableContentPresentation } from "../../shared/rendering/expandable-content-layout.ts";
2
+ import type { RenderTheme } from "../../shared/rendering/theme.ts";
3
+ import type { SendMessageViewModel } from "./send-message-view-model.ts";
4
+
5
+ export function buildSendMessagePresentation(
6
+ model: SendMessageViewModel,
7
+ theme: RenderTheme,
8
+ ): ExpandableContentPresentation {
9
+ const target =
10
+ model.status === "delivered"
11
+ ? (model.targetSessionName ?? model.targetSessionId)
12
+ : (model.targetSessionId ?? "[target pending]");
13
+ const metadata = [
14
+ model.relation,
15
+ model.requestResponse ? "response requested" : undefined,
16
+ ].filter((item): item is string => Boolean(item));
17
+ const metadataHint = metadata.length > 0 ? ` (${metadata.join(", ")})` : "";
18
+ const action = model.status === "delivered" ? "delivered to" : "to";
19
+
20
+ return {
21
+ header: `${theme.fg("toolTitle", theme.bold("session_send_message"))} ${theme.fg(
22
+ "muted",
23
+ `${action} ${target}${metadataHint}`,
24
+ )}`,
25
+ ...(model.status === "delivered"
26
+ ? {
27
+ expandedMetadata: [`${theme.bold("session")} ${model.targetSessionId}`],
28
+ }
29
+ : {}),
30
+ ...(model.body
31
+ ? {
32
+ body: {
33
+ text: theme.fg("toolOutput", model.body.replaceAll("\t", " ")),
34
+ collapsedRows: 3,
35
+ },
36
+ }
37
+ : {}),
38
+ };
39
+ }
@@ -0,0 +1,54 @@
1
+ import { normalizeOptionalText } from "../../shared/text.ts";
2
+ import type { SendMessageToolDetails } from "./message-contracts.ts";
3
+
4
+ export interface SendMessageRenderArgs {
5
+ session?: string | undefined;
6
+ message?: string | undefined;
7
+ requestResponse?: boolean | undefined;
8
+ }
9
+
10
+ export interface SendingMessageViewModel {
11
+ status: "sending";
12
+ targetSessionId?: string | undefined;
13
+ relation?: string | undefined;
14
+ requestResponse: boolean;
15
+ body: string;
16
+ }
17
+
18
+ export interface DeliveredMessageViewModel {
19
+ status: "delivered";
20
+ targetSessionId: string;
21
+ targetSessionName?: string | undefined;
22
+ relation?: string | undefined;
23
+ requestResponse: boolean;
24
+ body: string;
25
+ }
26
+
27
+ export type SendMessageViewModel = SendingMessageViewModel | DeliveredMessageViewModel;
28
+
29
+ export function buildSendingMessageView(
30
+ args: SendMessageRenderArgs | undefined,
31
+ relation: string | undefined,
32
+ ): SendingMessageViewModel {
33
+ return {
34
+ status: "sending",
35
+ targetSessionId: normalizeOptionalText(args?.session),
36
+ relation,
37
+ requestResponse: args?.requestResponse === true,
38
+ body: args?.message ?? "",
39
+ };
40
+ }
41
+
42
+ export function buildDeliveredMessageView(
43
+ args: SendMessageRenderArgs | undefined,
44
+ details: SendMessageToolDetails,
45
+ ): DeliveredMessageViewModel {
46
+ return {
47
+ status: "delivered",
48
+ targetSessionId: details.target.sessionId,
49
+ targetSessionName: normalizeOptionalText(details.target.sessionName),
50
+ relation: details.relation,
51
+ requestResponse: args?.requestResponse === true,
52
+ body: args?.message?.trim() ?? "",
53
+ };
54
+ }