pim-agent 0.9.0 → 0.10.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 (56) hide show
  1. package/bin/pim.ts +1 -1
  2. package/package.json +2 -2
  3. package/packages/core/src/extensions/CoreExtensions.ts +19 -16
  4. package/packages/core/src/extensions/apply-patch/render.ts +16 -55
  5. package/packages/core/src/extensions/system-prompt/index.ts +29 -21
  6. package/packages/core/src/extensions/system-prompt/prompt.ts +5 -1
  7. package/packages/core/src/picker/PickerService.ts +23 -1
  8. package/packages/core/src/picker/token.ts +12 -5
  9. package/packages/core/src/session/EventLog.ts +22 -79
  10. package/packages/core/src/session/SearchIndex.ts +752 -0
  11. package/packages/core/src/session/SearchTokens.ts +97 -0
  12. package/packages/core/src/session/SessionDigest.ts +161 -0
  13. package/packages/core/src/session/SessionHost.ts +17 -1
  14. package/packages/core/src/session/SessionMeta.ts +267 -0
  15. package/packages/core/src/session/SessionName.ts +18 -0
  16. package/packages/core/src/session/SessionRegistry.ts +89 -14
  17. package/packages/core/src/session/WriteMark.ts +15 -1
  18. package/packages/core/src/shared/DiffLines.ts +1 -1
  19. package/packages/core/src/shared/DiffPatch.ts +70 -0
  20. package/packages/core/src/shared/DiffRenderer.ts +1 -1
  21. package/packages/core/src/shared/FileWatch.ts +15 -1
  22. package/packages/core/src/shared/Git.ts +483 -42
  23. package/packages/core/src/shared/GitMonitor.ts +221 -0
  24. package/packages/core/src/shared/Levenshtein.ts +54 -1
  25. package/packages/core/src/shared/Proc.ts +21 -1
  26. package/packages/core/src/shared/RepoDiff.ts +741 -0
  27. package/packages/core/src/shared/Surface.ts +2 -0
  28. package/packages/core/src/shared/Updater.ts +21 -5
  29. package/packages/core/src/view/DiffExpand.ts +214 -0
  30. package/packages/core/src/view/DiffLayout.ts +7 -7
  31. package/packages/core/src/view/DiffPairs.ts +47 -0
  32. package/packages/core/src/view/MovePath.ts +70 -0
  33. package/packages/daemon/src/WebSurface.ts +1 -2
  34. package/packages/protocol/src/Command.ts +129 -2
  35. package/packages/protocol/src/Diff.ts +15 -0
  36. package/packages/protocol/src/ServerEvent.ts +157 -2
  37. package/packages/server/src/ClientConnection.ts +15 -3
  38. package/packages/server/src/ProbeClient.ts +90 -28
  39. package/packages/server/src/SessionCatalogue.ts +292 -38
  40. package/packages/server/src/SessionStream.ts +69 -43
  41. package/packages/server/src/StaticClient.ts +95 -12
  42. package/packages/server/src/WsGateway.ts +264 -29
  43. package/packages/server/src/probe.ts +61 -7
  44. package/packages/telegram/src/Session.ts +1 -0
  45. package/packages/tui/src/extensions/command-picker/index.ts +64 -32
  46. package/packages/tui/src/extensions/footer/index.ts +19 -43
  47. package/packages/tui/src/extensions/session-lease/index.ts +8 -3
  48. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-B-iV2FbL.woff2 +0 -0
  49. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +0 -0
  50. package/packages/web/dist/client/assets/{core-CGdQx9la.js → core-CA0aSzPu.js} +1 -1
  51. package/packages/web/dist/client/assets/index-B9be-i9d.css +1 -0
  52. package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -0
  53. package/packages/web/dist/client/index.html +2 -2
  54. package/packages/protocol/src/Protocol.ts +0 -6
  55. package/packages/web/dist/client/assets/index-BMNwleWO.js +0 -40
  56. package/packages/web/dist/client/assets/index-CqjB2RPr.css +0 -1
@@ -0,0 +1,2 @@
1
+ /** Where the human on the other end is reading; absent for a subagent, which has no human. */
2
+ export type Surface = "terminal" | "web browser" | "Telegram";
@@ -43,6 +43,8 @@ export type UpdateFacts = {
43
43
  readonly packageName: string;
44
44
  /** A dev checkout with no uncommitted changes; irrelevant to a prod install. */
45
45
  readonly cleanTree: boolean;
46
+ /** The dev checkout's branch tracks a remote one, so a pull has somewhere to pull from; irrelevant to a prod install. */
47
+ readonly tracked: boolean;
46
48
  /** The newest published release, or undefined when the registry was silent. */
47
49
  readonly latest: string | undefined;
48
50
  };
@@ -81,13 +83,24 @@ async function swapClient(packageRoot: string): Promise<void> {
81
83
  await rename(dirs.staging, dirs.client);
82
84
  }
83
85
 
86
+ /** Why this checkout cannot be pulled, or undefined when it can. */
87
+ function pullBlocker(facts: UpdateFacts): string | undefined {
88
+ if (!facts.tracked) {
89
+ return "this branch has no upstream to pull from";
90
+ }
91
+ return facts.cleanTree
92
+ ? undefined
93
+ : "the working tree has uncommitted changes";
94
+ }
95
+
84
96
  function plan(facts: UpdateFacts): UpdatePlan {
85
97
  const steps: UpdateStep[] = [];
86
98
  const skipped: UpdateSkip[] = [];
87
- const { at, packageName, cleanTree, latest } = facts;
99
+ const { at, packageName, latest } = facts;
88
100
 
89
101
  if (at.kind === "dev") {
90
- if (cleanTree) {
102
+ const blocked = pullBlocker(facts);
103
+ if (blocked === undefined) {
91
104
  steps.push({
92
105
  label: "git pull",
93
106
  command: ["git", "pull", "--ff-only"],
@@ -96,7 +109,7 @@ function plan(facts: UpdateFacts): UpdatePlan {
96
109
  } else {
97
110
  skipped.push({
98
111
  label: "git pull",
99
- reason: "the working tree has uncommitted changes",
112
+ reason: blocked,
100
113
  blocking: false,
101
114
  });
102
115
  }
@@ -137,14 +150,17 @@ function plan(facts: UpdateFacts): UpdatePlan {
137
150
 
138
151
  async function gather(): Promise<UpdateFacts> {
139
152
  const at = await Supervisor.detectInstall();
140
- const [packageName, cleanTree, latest] = await Promise.all([
153
+ const [packageName, cleanTree, tracked, latest] = await Promise.all([
141
154
  PimVersion.name(),
142
155
  at.kind === "dev"
143
156
  ? Git.fetchStatus(at.packageRoot).then((git) => git.dirtyCount === 0)
144
157
  : Promise.resolve(true),
158
+ at.kind === "dev"
159
+ ? Git.upstreamOf(at.packageRoot).then((at) => at !== undefined)
160
+ : Promise.resolve(true),
145
161
  at.kind === "prod" ? PimVersion.latest() : Promise.resolve(undefined),
146
162
  ]);
147
- return { at, packageName, cleanTree, latest };
163
+ return { at, packageName, cleanTree, tracked, latest };
148
164
  }
149
165
 
150
166
  /** Never restarts or exits: the caller owns that. */
@@ -0,0 +1,214 @@
1
+ import type { ToolDiffHunk, ToolDiffLine } from "../shared/DiffLines";
2
+ import type { LineSpan } from "../shared/RepoDiff";
3
+
4
+ /**
5
+ * A stretch of a file neither side changed and no hunk shows, numbered on the
6
+ * new side. `above` and `below` say which ends of it sit against a hunk, which
7
+ * is where a reader asking for more of it is given it.
8
+ */
9
+ export type DiffGap = {
10
+ readonly start: number;
11
+ readonly end: number;
12
+ readonly count: number;
13
+ readonly above: boolean;
14
+ readonly below: boolean;
15
+ };
16
+
17
+ /** What a diff is painted from, top to bottom: hunks and the gaps between them. */
18
+ export type DiffPart =
19
+ | { readonly hunk: ToolDiffHunk }
20
+ | { readonly gap: DiffGap };
21
+
22
+ /** Lines one click reveals at each end of a gap. */
23
+ const STEP = 25;
24
+
25
+ /** A remainder this small is taken with the click rather than left as a row of its own. */
26
+ const MIN_REMAINDER = 10;
27
+
28
+ function newEnd(hunk: ToolDiffHunk): number {
29
+ return hunk.newStart + hunk.newLines - 1;
30
+ }
31
+
32
+ function oldEnd(hunk: ToolDiffHunk): number {
33
+ return hunk.oldStart + hunk.oldLines - 1;
34
+ }
35
+
36
+ function gapOf(
37
+ start: number,
38
+ end: number,
39
+ above: boolean,
40
+ below: boolean
41
+ ): readonly DiffGap[] {
42
+ return end < start
43
+ ? []
44
+ : [{ start, end, count: end - start + 1, above, below }];
45
+ }
46
+
47
+ /**
48
+ * Every gap in one file's hunks: before the first, between each pair, and
49
+ * after the last once `total` says where the file ends. A file with no new
50
+ * side to number has none of them.
51
+ */
52
+ function gaps(
53
+ hunks: readonly ToolDiffHunk[],
54
+ total?: number
55
+ ): readonly DiffGap[] {
56
+ const first = hunks[0];
57
+ const last = hunks.at(-1);
58
+ if (first === undefined || last === undefined) {
59
+ return [];
60
+ }
61
+
62
+ return [
63
+ ...gapOf(1, first.newStart - 1, false, true),
64
+ ...hunks.flatMap((hunk, index) => {
65
+ const next = hunks[index + 1];
66
+ return next === undefined
67
+ ? []
68
+ : gapOf(newEnd(hunk) + 1, next.newStart - 1, true, true);
69
+ }),
70
+ ...(total === undefined ? [] : gapOf(newEnd(last) + 1, total, true, false)),
71
+ ];
72
+ }
73
+
74
+ /**
75
+ * What to ask the server for when a reader opens a gap: a step against each
76
+ * hunk it touches, or the whole gap when what would be left of it is a sliver.
77
+ */
78
+ function spans(gap: DiffGap): readonly LineSpan[] {
79
+ const edges = (gap.above ? 1 : 0) + (gap.below ? 1 : 0);
80
+ if (gap.count <= edges * STEP + MIN_REMAINDER) {
81
+ return [{ start: gap.start, end: gap.end }];
82
+ }
83
+ return [
84
+ ...(gap.above ? [{ start: gap.start, end: gap.start + STEP - 1 }] : []),
85
+ ...(gap.below ? [{ start: gap.end - STEP + 1, end: gap.end }] : []),
86
+ ];
87
+ }
88
+
89
+ /** How many lines opening a gap would put on the page. */
90
+ function revealed(gap: DiffGap): number {
91
+ return spans(gap).reduce(
92
+ (total, span) => total + span.end - span.start + 1,
93
+ 0
94
+ );
95
+ }
96
+
97
+ /** The hunks with each gap in its place among them. */
98
+ function parts(
99
+ hunks: readonly ToolDiffHunk[],
100
+ total?: number
101
+ ): readonly DiffPart[] {
102
+ const found = gaps(hunks, total);
103
+ const tail = found.find((gap) => !gap.below);
104
+ // Keyed by the line a gap ends on, which is the line before the hunk it
105
+ // leads into: one lookup per hunk rather than a scan of every gap.
106
+ const leading = new Map(found.map((gap) => [gap.end, gap]));
107
+ return [
108
+ ...hunks.flatMap((hunk) => {
109
+ const gap = leading.get(hunk.newStart - 1);
110
+ return gap === undefined ? [{ hunk }] : [{ gap }, { hunk }];
111
+ }),
112
+ ...(tail === undefined ? [] : [{ gap: tail }]),
113
+ ];
114
+ }
115
+
116
+ type Run = {
117
+ readonly oldStart: number;
118
+ readonly newStart: number;
119
+ readonly lines: ToolDiffLine[];
120
+ /** Lines of the run that stand on the new side, counted as they are added. */
121
+ newCount: number;
122
+ };
123
+
124
+ function context(line: number, offset: number, text: string): ToolDiffLine {
125
+ return { kind: "context", oldLine: line + offset, newLine: line, text };
126
+ }
127
+
128
+ function newLinesOf(lines: readonly ToolDiffLine[]): number {
129
+ return lines.filter((line) => line.kind !== "removed").length;
130
+ }
131
+
132
+ function hunkOf(run: Run): ToolDiffHunk {
133
+ return {
134
+ oldStart: run.oldStart,
135
+ newStart: run.newStart,
136
+ oldLines: run.lines.filter((line) => line.kind !== "added").length,
137
+ newLines: run.newCount,
138
+ lines: run.lines,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * The hunks with every line a reader has since been given spliced back into
144
+ * the gaps around them, and hunks that now touch merged into one. Only lines
145
+ * running from a hunk's own edge are taken: an island in the middle of a gap
146
+ * is not a thing this ever asks for.
147
+ */
148
+ function expand(
149
+ hunks: readonly ToolDiffHunk[],
150
+ known: ReadonlyMap<number, string>
151
+ ): readonly ToolDiffHunk[] {
152
+ if (known.size === 0) {
153
+ return hunks;
154
+ }
155
+
156
+ const runs: Run[] = [];
157
+
158
+ const reached = (): number => {
159
+ const last = runs.at(-1);
160
+ return last === undefined ? 0 : last.newStart + last.newCount - 1;
161
+ };
162
+
163
+ const add = (run: Run): void => {
164
+ const last = runs.at(-1);
165
+ if (last !== undefined && reached() + 1 === run.newStart) {
166
+ last.lines.push(...run.lines);
167
+ last.newCount += run.newCount;
168
+ return;
169
+ }
170
+ runs.push(run);
171
+ };
172
+
173
+ for (const [index, hunk] of hunks.entries()) {
174
+ const before: ToolDiffLine[] = [];
175
+ const floor = reached();
176
+ for (let line = hunk.newStart - 1; line > floor; line -= 1) {
177
+ const text = known.get(line);
178
+ if (text === undefined) {
179
+ break;
180
+ }
181
+ before.push(context(line, hunk.oldStart - hunk.newStart, text));
182
+ }
183
+ before.reverse();
184
+ const lines = [...before, ...hunk.lines];
185
+ add({
186
+ oldStart: hunk.oldStart - before.length,
187
+ newStart: hunk.newStart - before.length,
188
+ lines,
189
+ newCount: before.length + newLinesOf(hunk.lines),
190
+ });
191
+
192
+ const after: ToolDiffLine[] = [];
193
+ const ceiling = hunks[index + 1]?.newStart ?? Infinity;
194
+ for (let line = newEnd(hunk) + 1; line < ceiling; line += 1) {
195
+ const text = known.get(line);
196
+ if (text === undefined) {
197
+ break;
198
+ }
199
+ after.push(context(line, oldEnd(hunk) - newEnd(hunk), text));
200
+ }
201
+ if (after.length > 0) {
202
+ add({
203
+ oldStart: oldEnd(hunk) + 1,
204
+ newStart: newEnd(hunk) + 1,
205
+ lines: after,
206
+ newCount: after.length,
207
+ });
208
+ }
209
+ }
210
+
211
+ return runs.map(hunkOf);
212
+ }
213
+
214
+ export const DiffExpand = { gaps, parts, spans, revealed, expand };
@@ -1,4 +1,4 @@
1
- import type { ToolDiffHunk } from "../shared/DiffLines";
1
+ import type { ToolDiffHunk, ToolDiffLine } from "../shared/DiffLines";
2
2
 
3
3
  const TAB = " ";
4
4
 
@@ -8,7 +8,7 @@ function detab(text: string): string {
8
8
 
9
9
  // Highlight each side as one block, never line by line, or multi-line strings and comments mis-tokenise.
10
10
  function mapSides<T>(
11
- hunk: ToolDiffHunk,
11
+ lines: readonly ToolDiffLine[],
12
12
  highlight: (block: string) => readonly T[]
13
13
  ): readonly (T | undefined)[] {
14
14
  const oldIndices: (number | undefined)[] = [];
@@ -16,7 +16,7 @@ function mapSides<T>(
16
16
  const oldBlock: string[] = [];
17
17
  const newBlock: string[] = [];
18
18
 
19
- for (const line of hunk.lines) {
19
+ for (const line of lines) {
20
20
  // A context line must hold a slot in both blocks to keep tokeniser state aligned.
21
21
  if (line.kind !== "added") {
22
22
  oldIndices.push(oldBlock.length);
@@ -33,11 +33,11 @@ function mapSides<T>(
33
33
  }
34
34
  }
35
35
 
36
- const oldLines = oldBlock.length === 0 ? [] : highlight(oldBlock.join("\n"));
37
- const newLines = newBlock.length === 0 ? [] : highlight(newBlock.join("\n"));
36
+ const oldText = oldBlock.length === 0 ? [] : highlight(oldBlock.join("\n"));
37
+ const newText = newBlock.length === 0 ? [] : highlight(newBlock.join("\n"));
38
38
 
39
- return hunk.lines.map((line, index) => {
40
- const side = line.kind === "removed" ? oldLines : newLines;
39
+ return lines.map((line, index) => {
40
+ const side = line.kind === "removed" ? oldText : newText;
41
41
  const at = line.kind === "removed" ? oldIndices[index] : newIndices[index];
42
42
  return at === undefined ? undefined : side[at];
43
43
  });
@@ -0,0 +1,47 @@
1
+ import type { ToolDiffHunk, ToolDiffLine } from "../shared/DiffLines";
2
+
3
+ /** One row of a split view: the old side, the new side, or both. */
4
+ export type DiffPair = {
5
+ readonly left?: ToolDiffLine;
6
+ readonly right?: ToolDiffLine;
7
+ };
8
+
9
+ function pair(hunk: ToolDiffHunk): readonly DiffPair[] {
10
+ const pairs: DiffPair[] = [];
11
+ let removed: ToolDiffLine[] = [];
12
+ let added: ToolDiffLine[] = [];
13
+
14
+ const zip = (): void => {
15
+ const rows = Math.max(removed.length, added.length);
16
+ for (let index = 0; index < rows; index += 1) {
17
+ pairs.push({ left: removed[index], right: added[index] });
18
+ }
19
+ removed = [];
20
+ added = [];
21
+ };
22
+
23
+ for (const line of hunk.lines) {
24
+ if (line.kind === "context") {
25
+ zip();
26
+ pairs.push({ left: line, right: line });
27
+ continue;
28
+ }
29
+
30
+ if (line.kind === "added") {
31
+ added.push(line);
32
+ continue;
33
+ }
34
+
35
+ // A removal after an addition starts a new replacement rather than joining the last.
36
+ if (added.length > 0) {
37
+ zip();
38
+ }
39
+ removed.push(line);
40
+ }
41
+
42
+ zip();
43
+
44
+ return pairs;
45
+ }
46
+
47
+ export const DiffPairs = { pair };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * A file that moved, read as one path rather than two. Everything the old and
3
+ * new paths agree on is said once, and only the segments that actually changed
4
+ * are named on both sides — `packages/{web ➝ tui}/src/App.tsx` rather than two
5
+ * near-identical paths a reader has to diff by eye.
6
+ */
7
+ export type MoveParts = {
8
+ /** The leading directories both paths share, with its trailing `/`. */
9
+ readonly prefix: string;
10
+ /** What those segments used to be. */
11
+ readonly from: string;
12
+ /** What they are now. */
13
+ readonly to: string;
14
+ /** The trailing segments both paths share, with its leading `/`. */
15
+ readonly suffix: string;
16
+ };
17
+
18
+ /** The arrow every surface draws a move with. */
19
+ const ARROW = "➝";
20
+
21
+ /**
22
+ * The shared head and tail of a move, or `undefined` when there is nothing
23
+ * worth folding: two paths that share no segment read better whole, since a
24
+ * brace around the entire path only adds punctuation. Two bare filenames are
25
+ * the exception — `{old.ts ➝ new.ts}` shares no segment but is still one name.
26
+ */
27
+ function fold(oldPath: string, newPath: string): MoveParts | undefined {
28
+ const oldParts = oldPath.split("/");
29
+ const newParts = newPath.split("/");
30
+
31
+ let head = 0;
32
+ while (
33
+ head < oldParts.length &&
34
+ head < newParts.length &&
35
+ oldParts[head] === newParts[head]
36
+ ) {
37
+ head += 1;
38
+ }
39
+
40
+ let tail = 0;
41
+ while (
42
+ tail < oldParts.length - head &&
43
+ tail < newParts.length - head &&
44
+ oldParts[oldParts.length - tail - 1] ===
45
+ newParts[newParts.length - tail - 1]
46
+ ) {
47
+ tail += 1;
48
+ }
49
+
50
+ const from = oldParts.slice(head, oldParts.length - tail);
51
+ const to = newParts.slice(head, newParts.length - tail);
52
+ const bareNames = oldParts.length === 1 && newParts.length === 1;
53
+
54
+ if (
55
+ from.length === 0 ||
56
+ to.length === 0 ||
57
+ (head === 0 && tail === 0 && !bareNames)
58
+ ) {
59
+ return undefined;
60
+ }
61
+
62
+ return {
63
+ prefix: head > 0 ? `${oldParts.slice(0, head).join("/")}/` : "",
64
+ from: from.join("/"),
65
+ to: to.join("/"),
66
+ suffix: tail > 0 ? `/${oldParts.slice(-tail).join("/")}` : "",
67
+ };
68
+ }
69
+
70
+ export const MovePath = { ARROW, fold };
@@ -25,8 +25,7 @@ function create(args: ReadonlyArray<string>, runtime: AgentRuntime): Surface {
25
25
  SendFileTool.build({ store, cwd, sessionId })
26
26
  ) as ToolDefinition,
27
27
  ],
28
- systemInstruction: async () =>
29
- "The user is interacting with you via a web browser.",
28
+ surface: "web browser",
30
29
  });
31
30
  await registry.init();
32
31
 
@@ -1,4 +1,4 @@
1
- import type { ProtocolVersion } from "./Protocol";
1
+ import type { DiffBase, LineSpan } from "./Diff";
2
2
 
3
3
  /** A file already uploaded via `POST /upload`, by the id that endpoint answered with; never a client-local path. */
4
4
  export type AttachmentRef = {
@@ -11,13 +11,16 @@ export type Command =
11
11
  | {
12
12
  readonly id: string;
13
13
  readonly type: "attach";
14
- readonly protocolVersion: ProtocolVersion;
15
14
  readonly sessionId?: string;
16
15
  readonly cwd?: string;
17
16
  /** Copy model, thinking level and cwd from this session; ignored when `sessionId` is set or the session is not held open. */
18
17
  readonly like?: string;
19
18
  readonly fromSeq: number;
19
+ /** Whether the client is looking at this session right now; absent means yes. */
20
+ readonly attentive?: boolean;
20
21
  }
22
+ /** Whether this connection's reader is present: an inattentive one never consumes a turn as read. */
23
+ | { readonly id: string; readonly type: "attention"; readonly value: boolean }
21
24
  /** Sent into a running turn it steers that turn, landing before the next model call. */
22
25
  | {
23
26
  readonly id: string;
@@ -60,11 +63,123 @@ export type Command =
60
63
  /** Restrict to one working directory; omit for every session on disk. */
61
64
  readonly cwd?: string;
62
65
  readonly limit?: number;
66
+ /** Keep at most this many sessions per working directory, so one busy project cannot fill the page. */
67
+ readonly perProject?: number;
68
+ /** List the archived sessions instead of the live ones. */
69
+ readonly archived?: boolean;
70
+ }
71
+ /** Ranked search over every session on disk, titles and what was said; an empty `query` warms the index and answers with no hits. */
72
+ | {
73
+ readonly id: string;
74
+ readonly type: "search_sessions";
75
+ readonly query: string;
76
+ readonly limit?: number;
77
+ /** Restrict to one working directory; omit for every session on disk. */
78
+ readonly cwd?: string;
79
+ /** Omitted, the archived are searched too and their hits say so; `false` leaves them out, `true` searches only them. */
80
+ readonly archived?: boolean;
81
+ }
82
+ /** Names a session through pi's own `session_info`, so its terminal picker shows the name too; `null` clears it. */
83
+ | {
84
+ readonly id: string;
85
+ readonly type: "set_session_name";
86
+ readonly sessionId: string;
87
+ readonly value: string | null;
88
+ }
89
+ /** pim's own overrides on a session: out of the default listing, or held unread until it is answered. */
90
+ | {
91
+ readonly id: string;
92
+ readonly type: "set_session_archived" | "set_session_unread";
93
+ readonly sessionId: string;
94
+ readonly value: boolean;
95
+ }
96
+ /**
97
+ * pim's own overrides on a working directory rather than a session: pinned
98
+ * sorts it above every other, expanded stands its sidebar group unfolded.
99
+ */
100
+ | {
101
+ readonly id: string;
102
+ readonly type: "set_project_pinned" | "set_project_expanded";
103
+ readonly cwd: string;
104
+ readonly value: boolean;
105
+ }
106
+ /** Names a working directory for the listings; `null` puts it back to its base name, and the directory is never renamed. */
107
+ | {
108
+ readonly id: string;
109
+ readonly type: "set_project_label";
110
+ readonly cwd: string;
111
+ readonly value: string | null;
112
+ }
113
+ /** Re-orders the pinned projects. The whole order, never a move: two surfaces settle on the last one sent. */
114
+ | {
115
+ readonly id: string;
116
+ readonly type: "set_pin_order";
117
+ readonly order: readonly string[];
63
118
  }
64
119
  /** The models this server can switch to, plus the current model's thinking levels; answers without a session. */
65
120
  | { readonly id: string; readonly type: "list_models" }
66
121
  /** Subdirectories of `path` on the server's filesystem; errors rather than answering empty when it is not a readable directory. */
67
122
  | { readonly id: string; readonly type: "list_dirs"; readonly path: string }
123
+ /** Re-read the cwd's git state now; `fetch` asks the remote first, which is the only thing that moves ahead and behind. */
124
+ | {
125
+ readonly id: string;
126
+ readonly type: "refresh_git";
127
+ readonly sessionId: string;
128
+ readonly fetch?: boolean;
129
+ }
130
+ /** The cwd's local branches, trunk first and the rest by how recently they were worked on. */
131
+ | {
132
+ readonly id: string;
133
+ readonly type: "list_branches";
134
+ readonly sessionId: string;
135
+ }
136
+ /** Every changed file of one diff base, without a hunk of any of them; read-only, so never refused. */
137
+ | {
138
+ readonly id: string;
139
+ readonly type: "list_changes";
140
+ readonly sessionId: string;
141
+ readonly base: DiffBase;
142
+ }
143
+ /** One file's hunks, computed only once a reader expands it; `context` defaults to 3. */
144
+ | {
145
+ readonly id: string;
146
+ readonly type: "file_diff";
147
+ readonly sessionId: string;
148
+ readonly base: DiffBase;
149
+ readonly path: string;
150
+ readonly context?: number;
151
+ }
152
+ /** The file's own lines behind a gap between hunks, asked for when a reader opens one. */
153
+ | {
154
+ readonly id: string;
155
+ readonly type: "read_lines";
156
+ readonly sessionId: string;
157
+ readonly base: DiffBase;
158
+ readonly path: string;
159
+ readonly spans: readonly LineSpan[];
160
+ }
161
+ /** Refused while any session in the same directory is mid-turn: the agent may be halfway through an edit. */
162
+ | {
163
+ readonly id: string;
164
+ readonly type: "checkout";
165
+ readonly sessionId: string;
166
+ readonly branch: string;
167
+ }
168
+ /** `pull` is fast-forward only; `push` adopts an upstream the first time a branch is published. */
169
+ | {
170
+ readonly id: string;
171
+ readonly type: "pull" | "push";
172
+ readonly sessionId: string;
173
+ }
174
+ /** Path-limited: exactly `paths` are staged and committed, everything else changed stays dirty. Refused mid-turn, like `checkout`. */
175
+ | {
176
+ readonly id: string;
177
+ readonly type: "commit";
178
+ readonly sessionId: string;
179
+ readonly message: string;
180
+ /** A renamed file contributes both of its names, or its old one is left behind. */
181
+ readonly paths: readonly string[];
182
+ }
68
183
  /** Read-only view of a subagent's transcript; `callId` is the parent's tool call and `sessionId` must be this connection's session. */
69
184
  | {
70
185
  readonly id: string;
@@ -90,3 +205,15 @@ export type CommandDraft = Command extends infer T
90
205
  ? Omit<T, "id">
91
206
  : never
92
207
  : never;
208
+
209
+ /** Which sessions a listing is asking for: the `list_sessions` payload, named so a client can pass it around. */
210
+ export type SessionScope = Omit<
211
+ Extract<CommandDraft, { readonly type: "list_sessions" }>,
212
+ "type"
213
+ >;
214
+
215
+ /** Which sessions a search may reach: the `search_sessions` payload without the query it is asked with. */
216
+ export type SearchScope = Omit<
217
+ Extract<CommandDraft, { readonly type: "search_sessions" }>,
218
+ "type" | "query"
219
+ >;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The diff wire shape. Declared beside the git that produces it, the way
3
+ * `GitBranch` and `ToolView` are, and named here so the two commands that
4
+ * carry it read as protocol.
5
+ */
6
+ export type {
7
+ ChangeList,
8
+ ChangeStatus,
9
+ ChangeSummary,
10
+ DiffBase,
11
+ FileLines,
12
+ FileDiff,
13
+ LineRun,
14
+ LineSpan,
15
+ } from "#core/shared/RepoDiff";