@zetaloop/chappie 0.3.1 → 0.4.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/src/session.ts CHANGED
@@ -13,7 +13,9 @@ import type {
13
13
  SessionEntry,
14
14
  } from "@earendil-works/pi-coding-agent";
15
15
  import { Text } from "@earendil-works/pi-tui";
16
+ import { type Activity, chatLabel, source } from "./activity.ts";
16
17
  import type { DeliveryRecord } from "./delivery.ts";
18
+ import { historyResult } from "./history.ts";
17
19
  import {
18
20
  type BrokerMessage,
19
21
  IpcClient,
@@ -27,7 +29,7 @@ import { readSessionResource, rememberImages } from "./resources.ts";
27
29
 
28
30
  type RemoteRequest = Extract<BrokerMessage, { type: "chat" | "call" }>;
29
31
 
30
- interface Notice {
32
+ interface Notice extends Activity {
31
33
  message: string;
32
34
  type: "info" | "warning" | "error";
33
35
  }
@@ -126,9 +128,17 @@ export class LocalSession {
126
128
  this.#pi.on("session_shutdown", () => this.close());
127
129
  }
128
130
 
129
- #notify(message: string, type: Notice["type"] = "info"): void {
131
+ #notify(
132
+ message: string,
133
+ type: Notice["type"] = "info",
134
+ activity: Activity = {},
135
+ ): void {
130
136
  if (this.#context)
131
- this.#pi.appendEntry<Notice>("chappie.notice", { message, type });
137
+ this.#pi.appendEntry<Notice>("chappie.notice", {
138
+ message,
139
+ type,
140
+ ...activity,
141
+ });
132
142
  }
133
143
 
134
144
  async start(output: ProviderOutput): Promise<void> {
@@ -194,7 +204,6 @@ export class LocalSession {
194
204
  this.#resetInputs(context);
195
205
  }
196
206
  if (!this.#connection) {
197
- this.#notify("Connecting to Chappie…");
198
207
  this.#connection = new IpcClient(this.#agentDir, this.#connect, {
199
208
  onOpen: async () => {
200
209
  await this.#sync();
@@ -262,7 +271,7 @@ export class LocalSession {
262
271
  if (
263
272
  message.sessionId === this.#context?.sessionManager.getSessionId()
264
273
  ) {
265
- this.#notify(message.message);
274
+ this.#notify(message.message, "info", message.activity);
266
275
  }
267
276
  break;
268
277
  case "inspect":
@@ -277,6 +286,26 @@ export class LocalSession {
277
286
  };
278
287
  });
279
288
  break;
289
+ case "history":
290
+ await this.#reply(message.id, message.sessionId, () => {
291
+ const context = this.#context;
292
+ if (!context) throw new Error("Chappie session is not available");
293
+ const history = historyResult(
294
+ context.sessionManager.getBranch(),
295
+ message.sessionId,
296
+ message.range,
297
+ );
298
+ this.#notify(
299
+ `${chatLabel(message)} read history: ${history.count} entries`,
300
+ "info",
301
+ {
302
+ event: "history",
303
+ ...source(message.chatId, message.requestId),
304
+ },
305
+ );
306
+ return { type: "result", id: message.id, cwd: context.cwd, history };
307
+ });
308
+ break;
280
309
  case "ackInputs":
281
310
  if (
282
311
  message.sessionId === this.#context?.sessionManager.getSessionId()
@@ -306,8 +335,12 @@ export class LocalSession {
306
335
  ? [...new Set(request.calls.map((call) => call.name))].join(", ")
307
336
  : "chat";
308
337
  this.#notify(
309
- `${name} cancelled for ChatGPT ${request.chatId.slice(-4)}: ${message.reason}`,
338
+ `${name} cancelled for ${chatLabel(request)}: ${message.reason}`,
310
339
  "warning",
340
+ {
341
+ event: "cancelled",
342
+ ...source(request.chatId, request.requestId),
343
+ },
311
344
  );
312
345
  if (queued !== -1) {
313
346
  this.#queue.splice(queued, 1);
@@ -371,6 +404,7 @@ export class LocalSession {
371
404
  }
372
405
 
373
406
  this.#queue.shift();
407
+ output.message.chappie = source(request.chatId, request.requestId);
374
408
  this.#active = {
375
409
  request,
376
410
  session: this.#description(),
@@ -432,7 +466,7 @@ export class LocalSession {
432
466
  if (active.cancelled !== undefined) {
433
467
  const delivery: DeliveryRecord = {
434
468
  id: randomUUID(),
435
- chatId: active.request.chatId,
469
+ ...source(active.request.chatId, active.request.requestId),
436
470
  sessionId: active.session.id,
437
471
  cwd: active.session.cwd,
438
472
  toolResults: active.toolResults,
@@ -516,7 +550,14 @@ export class LocalSession {
516
550
  await connection.send({ type: "delivery", delivery });
517
551
  await completion.promise;
518
552
  this.#deliveries.delete(delivery.id);
519
- this.#notify(`Result saved for ChatGPT ${delivery.chatId.slice(-4)}`);
553
+ this.#notify(
554
+ `Result saved for ChatGPT ${delivery.chatId.slice(-4)}`,
555
+ "info",
556
+ {
557
+ event: "result_saved",
558
+ ...source(delivery.chatId, delivery.requestId),
559
+ },
560
+ );
520
561
  } finally {
521
562
  this.#stores.delete(delivery.id);
522
563
  }
package/src/state.ts CHANGED
@@ -5,7 +5,7 @@ import type { QuestionAnswer, QuestionRecord } from "./questions.ts";
5
5
 
6
6
  interface StateFile {
7
7
  bindings?: Record<string, string>;
8
- workflows?: Record<string, string>;
8
+ codes?: Record<string, string>;
9
9
  deliveries?: DeliveryRecord[];
10
10
  questions?: QuestionRecord[];
11
11
  }
@@ -14,7 +14,7 @@ export class State {
14
14
  readonly #path: string;
15
15
  readonly #temporaryPath: string;
16
16
  readonly #bindings = new Map<string, string>();
17
- readonly #workflows = new Map<string, string>();
17
+ readonly #codes = new Map<string, string>();
18
18
  readonly #deliveries = new Map<string, DeliveryRecord>();
19
19
  readonly #questions = new Map<string, QuestionRecord>();
20
20
  #writes = Promise.resolve();
@@ -36,8 +36,8 @@ export class State {
36
36
  for (const [chatId, sessionId] of Object.entries(state.bindings ?? {})) {
37
37
  if (typeof sessionId === "string") this.#bindings.set(chatId, sessionId);
38
38
  }
39
- for (const [chatId, id] of Object.entries(state.workflows ?? {})) {
40
- this.#workflows.set(chatId, id);
39
+ for (const [sessionId, code] of Object.entries(state.codes ?? {})) {
40
+ this.#codes.set(sessionId, code);
41
41
  }
42
42
  for (const delivery of state.deliveries ?? []) {
43
43
  if (delivery?.id) this.#deliveries.set(delivery.id, delivery);
@@ -46,12 +46,13 @@ export class State {
46
46
  this.#questions.set(question.id, question);
47
47
  }
48
48
 
49
- workflow(chatId: string): string | undefined {
50
- return this.#workflows.get(chatId);
49
+ code(sessionId: string): string | undefined {
50
+ return this.#codes.get(sessionId);
51
51
  }
52
52
 
53
- setWorkflow(chatId: string, id: string): Promise<void> {
54
- this.#workflows.set(chatId, id);
53
+ setCode(sessionId: string, code: string | undefined): Promise<void> {
54
+ if (code === undefined) this.#codes.delete(sessionId);
55
+ else this.#codes.set(sessionId, code);
55
56
  return this.#save();
56
57
  }
57
58
 
@@ -59,12 +60,6 @@ export class State {
59
60
  return this.#bindings.get(chatId);
60
61
  }
61
62
 
62
- chats(sessionId: string): string[] {
63
- return [...this.#bindings]
64
- .filter(([, target]) => target === sessionId)
65
- .map(([chatId]) => chatId);
66
- }
67
-
68
63
  bindingCounts(): Map<string, number> {
69
64
  const counts = new Map<string, number>();
70
65
  for (const sessionId of this.#bindings.values()) {
@@ -73,7 +68,7 @@ export class State {
73
68
  return counts;
74
69
  }
75
70
 
76
- setBinding(chatId: string, sessionId: string): Promise<void> {
71
+ bind(chatId: string, sessionId: string): Promise<void> {
77
72
  this.#bindings.set(chatId, sessionId);
78
73
  return this.#save();
79
74
  }
@@ -168,10 +163,10 @@ export class State {
168
163
  #save(): Promise<void> {
169
164
  const saved = this.#writes.then(async () => {
170
165
  const bindings = Object.fromEntries(this.#bindings);
171
- const workflows = Object.fromEntries(this.#workflows);
166
+ const codes = Object.fromEntries(this.#codes);
172
167
  await writeFile(
173
168
  this.#temporaryPath,
174
- `${JSON.stringify({ bindings, workflows, deliveries: [...this.#deliveries.values()], questions: [...this.#questions.values()] }, null, 2)}\n`,
169
+ `${JSON.stringify({ bindings, codes, deliveries: [...this.#deliveries.values()], questions: [...this.#questions.values()] }, null, 2)}\n`,
175
170
  {
176
171
  mode: 0o600,
177
172
  },
package/src/tools.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  createWriteToolDefinition,
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import { fromJsonSchema } from "@modelcontextprotocol/server";
9
+ import type { Initialization } from "./broker.ts";
9
10
  import { toolResultsContent } from "./delivery.ts";
10
11
  import type { SessionInput } from "./ipc.ts";
11
12
  import { contentWithImageReferences } from "./resources.ts";
@@ -38,10 +39,18 @@ export function toolResult(
38
39
  sessionId: string,
39
40
  cwd: string,
40
41
  inputs: SessionInput[] = [],
42
+ initialization?: Initialization,
41
43
  ) {
42
44
  return {
43
45
  content: [
44
- { type: "text" as const, text: JSON.stringify({ sessionId, cwd }) },
46
+ {
47
+ type: "text" as const,
48
+ text: JSON.stringify({
49
+ sessionId,
50
+ cwd,
51
+ ...(initialization ? { initialization } : {}),
52
+ }),
53
+ },
45
54
  ...toolResultsContent(toolResults, sessionId),
46
55
  ...inputContent(inputs),
47
56
  ],
@@ -73,7 +82,8 @@ function withSessionId(
73
82
  ...copy.properties,
74
83
  sessionId: {
75
84
  type: "string",
76
- description: "Pi session for this operation only",
85
+ description:
86
+ "Pi session for this operation; becomes the default if none is set",
77
87
  },
78
88
  },
79
89
  additionalProperties: false,