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.
- package/bin/pim.ts +1 -1
- package/package.json +2 -2
- package/packages/core/src/extensions/CoreExtensions.ts +19 -16
- package/packages/core/src/extensions/apply-patch/render.ts +16 -55
- package/packages/core/src/extensions/system-prompt/index.ts +29 -21
- package/packages/core/src/extensions/system-prompt/prompt.ts +5 -1
- package/packages/core/src/picker/PickerService.ts +23 -1
- package/packages/core/src/picker/token.ts +12 -5
- package/packages/core/src/session/EventLog.ts +22 -79
- package/packages/core/src/session/SearchIndex.ts +752 -0
- package/packages/core/src/session/SearchTokens.ts +97 -0
- package/packages/core/src/session/SessionDigest.ts +161 -0
- package/packages/core/src/session/SessionHost.ts +17 -1
- package/packages/core/src/session/SessionMeta.ts +267 -0
- package/packages/core/src/session/SessionName.ts +18 -0
- package/packages/core/src/session/SessionRegistry.ts +89 -14
- package/packages/core/src/session/WriteMark.ts +15 -1
- package/packages/core/src/shared/DiffLines.ts +1 -1
- package/packages/core/src/shared/DiffPatch.ts +70 -0
- package/packages/core/src/shared/DiffRenderer.ts +1 -1
- package/packages/core/src/shared/FileWatch.ts +15 -1
- package/packages/core/src/shared/Git.ts +483 -42
- package/packages/core/src/shared/GitMonitor.ts +221 -0
- package/packages/core/src/shared/Levenshtein.ts +54 -1
- package/packages/core/src/shared/Proc.ts +21 -1
- package/packages/core/src/shared/RepoDiff.ts +741 -0
- package/packages/core/src/shared/Surface.ts +2 -0
- package/packages/core/src/shared/Updater.ts +21 -5
- package/packages/core/src/view/DiffExpand.ts +214 -0
- package/packages/core/src/view/DiffLayout.ts +7 -7
- package/packages/core/src/view/DiffPairs.ts +47 -0
- package/packages/core/src/view/MovePath.ts +70 -0
- package/packages/daemon/src/WebSurface.ts +1 -2
- package/packages/protocol/src/Command.ts +129 -2
- package/packages/protocol/src/Diff.ts +15 -0
- package/packages/protocol/src/ServerEvent.ts +157 -2
- package/packages/server/src/ClientConnection.ts +15 -3
- package/packages/server/src/ProbeClient.ts +90 -28
- package/packages/server/src/SessionCatalogue.ts +292 -38
- package/packages/server/src/SessionStream.ts +69 -43
- package/packages/server/src/StaticClient.ts +95 -12
- package/packages/server/src/WsGateway.ts +264 -29
- package/packages/server/src/probe.ts +61 -7
- package/packages/telegram/src/Session.ts +1 -0
- package/packages/tui/src/extensions/command-picker/index.ts +64 -32
- package/packages/tui/src/extensions/footer/index.ts +19 -43
- package/packages/tui/src/extensions/session-lease/index.ts +8 -3
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-B-iV2FbL.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +0 -0
- package/packages/web/dist/client/assets/{core-CGdQx9la.js → core-CA0aSzPu.js} +1 -1
- package/packages/web/dist/client/assets/index-B9be-i9d.css +1 -0
- package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -0
- package/packages/web/dist/client/index.html +2 -2
- package/packages/protocol/src/Protocol.ts +0 -6
- package/packages/web/dist/client/assets/index-BMNwleWO.js +0 -40
- package/packages/web/dist/client/assets/index-CqjB2RPr.css +0 -1
package/bin/pim.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { themeCliArgs } from "#tui/themes/themeCliArgs";
|
|
|
24
24
|
// `_init` must stay first: it installs the runtime guard.
|
|
25
25
|
const extensionFactories: readonly PimInlineExtension[] = [
|
|
26
26
|
{ name: "_init", factory: init },
|
|
27
|
-
...CoreExtensions.list,
|
|
27
|
+
...CoreExtensions.list("terminal"),
|
|
28
28
|
{ name: "command-picker", factory: commandPicker },
|
|
29
29
|
{ name: "file-picker", factory: filePicker },
|
|
30
30
|
{ name: "footer", factory: footer },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pim-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A batteries-included distro of Pi, accessible in your terminal, browser and Telegram.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"check": "bun scripts/check.ts",
|
|
53
53
|
"gen:brand": "bun scripts/brand/generate.ts",
|
|
54
54
|
"gen:hero": "bun scripts/hero/generate.ts",
|
|
55
|
-
"web:build": "cd packages/web && vite build",
|
|
55
|
+
"web:build": "cd packages/web && NODE_ENV=production vite build",
|
|
56
56
|
"web:dev": "cd packages/web && vite"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ExtensionToggles,
|
|
8
8
|
type PimExtensionName,
|
|
9
9
|
} from "../shared/ExtensionToggles";
|
|
10
|
+
import type { Surface } from "../shared/Surface";
|
|
10
11
|
import applyPatch from "./apply-patch/index";
|
|
11
12
|
import bash from "./bash/index";
|
|
12
13
|
import edit from "./edit/index";
|
|
@@ -26,23 +27,25 @@ export type PimInlineExtension = {
|
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
// Enumerated, never globbed: the published tarball must not depend on a directory scan.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
function list(surface?: Surface): readonly PimInlineExtension[] {
|
|
31
|
+
return [
|
|
32
|
+
{ name: "apply-patch", factory: applyPatch },
|
|
33
|
+
{ name: "bash", factory: bash },
|
|
34
|
+
{ name: "edit", factory: edit },
|
|
35
|
+
{ name: "glob", factory: glob },
|
|
36
|
+
{ name: "grep", factory: grep },
|
|
37
|
+
{ name: "read", factory: read },
|
|
38
|
+
{ name: "subagent", factory: subagent },
|
|
39
|
+
{ name: "system-prompt", factory: systemPrompt(surface) },
|
|
40
|
+
{ name: "todo", factory: todo },
|
|
41
|
+
{ name: "web-fetch", factory: webFetch },
|
|
42
|
+
{ name: "web-search", factory: webSearch },
|
|
43
|
+
{ name: "write", factory: write },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
43
46
|
|
|
44
|
-
function gated(): InlineExtension[] {
|
|
45
|
-
return list.map(({ name, factory }) => ({
|
|
47
|
+
function gated(surface?: Surface): InlineExtension[] {
|
|
48
|
+
return list(surface).map(({ name, factory }) => ({
|
|
46
49
|
name,
|
|
47
50
|
factory: ExtensionToggles.gate(name, factory),
|
|
48
51
|
}));
|
|
@@ -3,11 +3,12 @@ import { Paths } from "../../shared/Paths";
|
|
|
3
3
|
import { PatchSummary } from "../../shared/PatchSummary";
|
|
4
4
|
import type { ToolViewInput } from "../../shared/Tools";
|
|
5
5
|
import { DiffBlocks } from "../../view/DiffBlocks";
|
|
6
|
+
import { MovePath } from "../../view/MovePath";
|
|
6
7
|
import type { Span, ToolIcon, ToolView, ViewBlock } from "../../view/ViewBlock";
|
|
7
8
|
import type { ApplyEntry } from "./executor";
|
|
8
9
|
import { type applyPatchSchema, prepareApplyPatchArguments } from "./schema";
|
|
9
10
|
|
|
10
|
-
const ARROW =
|
|
11
|
+
const ARROW = MovePath.ARROW;
|
|
11
12
|
|
|
12
13
|
export type ApplyPatchDetails = {
|
|
13
14
|
readonly entries?: readonly ApplyEntry[];
|
|
@@ -152,65 +153,25 @@ function sectionBlock(entry: EntryView): ViewBlock {
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
function moveTitle(oldPath: string, newPath: string): readonly Span[] {
|
|
155
|
-
const
|
|
156
|
-
const newParts = newPath.split("/");
|
|
157
|
-
let commonPrefix = 0;
|
|
158
|
-
|
|
159
|
-
while (
|
|
160
|
-
commonPrefix < oldParts.length &&
|
|
161
|
-
commonPrefix < newParts.length &&
|
|
162
|
-
oldParts[commonPrefix] === newParts[commonPrefix]
|
|
163
|
-
) {
|
|
164
|
-
commonPrefix += 1;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let commonSuffix = 0;
|
|
168
|
-
while (
|
|
169
|
-
commonSuffix < oldParts.length - commonPrefix &&
|
|
170
|
-
commonSuffix < newParts.length - commonPrefix &&
|
|
171
|
-
oldParts[oldParts.length - commonSuffix - 1] ===
|
|
172
|
-
newParts[newParts.length - commonSuffix - 1]
|
|
173
|
-
) {
|
|
174
|
-
commonSuffix += 1;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const oldChanged = oldParts.slice(
|
|
178
|
-
commonPrefix,
|
|
179
|
-
oldParts.length - commonSuffix
|
|
180
|
-
);
|
|
181
|
-
const newChanged = newParts.slice(
|
|
182
|
-
commonPrefix,
|
|
183
|
-
newParts.length - commonSuffix
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
oldChanged.length > 0 &&
|
|
188
|
-
newChanged.length > 0 &&
|
|
189
|
-
(commonPrefix > 0 ||
|
|
190
|
-
commonSuffix > 0 ||
|
|
191
|
-
(oldParts.length === 1 && newParts.length === 1))
|
|
192
|
-
) {
|
|
193
|
-
const prefix =
|
|
194
|
-
commonPrefix > 0 ? `${oldParts.slice(0, commonPrefix).join("/")}/` : "";
|
|
195
|
-
const suffix =
|
|
196
|
-
commonSuffix > 0 ? `/${oldParts.slice(-commonSuffix).join("/")}` : "";
|
|
156
|
+
const folded = MovePath.fold(oldPath, newPath);
|
|
197
157
|
|
|
158
|
+
if (folded === undefined) {
|
|
198
159
|
return [
|
|
199
|
-
{ text:
|
|
200
|
-
{ text: "
|
|
201
|
-
{ text:
|
|
202
|
-
{ text:
|
|
203
|
-
{ text:
|
|
204
|
-
{ text: "}", tone: "dim" },
|
|
205
|
-
{ text: suffix, tone: "title" },
|
|
160
|
+
{ text: oldPath, tone: "dim", strike: true },
|
|
161
|
+
{ text: " " },
|
|
162
|
+
{ text: ARROW, tone: "dim" },
|
|
163
|
+
{ text: " " },
|
|
164
|
+
{ text: newPath, tone: "title" },
|
|
206
165
|
];
|
|
207
166
|
}
|
|
208
167
|
|
|
209
168
|
return [
|
|
210
|
-
{ text:
|
|
211
|
-
{ text: " " },
|
|
212
|
-
{ text:
|
|
213
|
-
{ text:
|
|
214
|
-
{ text:
|
|
169
|
+
{ text: folded.prefix },
|
|
170
|
+
{ text: "{", tone: "dim" },
|
|
171
|
+
{ text: folded.from, tone: "dim", strike: true },
|
|
172
|
+
{ text: ` ${ARROW} `, tone: "dim" },
|
|
173
|
+
{ text: folded.to, tone: "title" },
|
|
174
|
+
{ text: "}", tone: "dim" },
|
|
175
|
+
{ text: folded.suffix, tone: "title" },
|
|
215
176
|
];
|
|
216
177
|
}
|
|
@@ -1,28 +1,36 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionFactory,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import { formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import type { Surface } from "../../shared/Surface";
|
|
3
8
|
import { buildSystemPrompt } from "./prompt";
|
|
4
9
|
|
|
5
|
-
export default function (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
contextFiles,
|
|
10
|
-
skills,
|
|
11
|
-
promptGuidelines,
|
|
12
|
-
appendSystemPrompt,
|
|
13
|
-
customPrompt,
|
|
14
|
-
} = event.systemPromptOptions;
|
|
15
|
-
return {
|
|
16
|
-
systemPrompt: buildSystemPrompt({
|
|
17
|
-
model: ctx.model,
|
|
10
|
+
export default function (surface?: Surface): ExtensionFactory {
|
|
11
|
+
return (pi: ExtensionAPI): void => {
|
|
12
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
13
|
+
const {
|
|
18
14
|
cwd,
|
|
19
|
-
contextFiles
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
toolGuidelines: promptGuidelines ?? [],
|
|
15
|
+
contextFiles,
|
|
16
|
+
skills,
|
|
17
|
+
promptGuidelines,
|
|
23
18
|
appendSystemPrompt,
|
|
24
19
|
customPrompt,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
} = event.systemPromptOptions;
|
|
21
|
+
return {
|
|
22
|
+
systemPrompt: buildSystemPrompt({
|
|
23
|
+
model: ctx.model,
|
|
24
|
+
cwd,
|
|
25
|
+
contextFiles: contextFiles ?? [],
|
|
26
|
+
skillsBlock:
|
|
27
|
+
skills && skills.length > 0 ? formatSkillsForPrompt(skills) : "",
|
|
28
|
+
toolGuidelines: promptGuidelines ?? [],
|
|
29
|
+
appendSystemPrompt,
|
|
30
|
+
customPrompt,
|
|
31
|
+
...(surface === undefined ? {} : { surface }),
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
};
|
|
28
36
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
+
import type { Surface } from "../../shared/Surface";
|
|
4
|
+
|
|
3
5
|
type BuildOptions = {
|
|
4
6
|
readonly model?: ExtensionContext["model"];
|
|
5
7
|
readonly cwd: string;
|
|
@@ -12,6 +14,7 @@ type BuildOptions = {
|
|
|
12
14
|
readonly appendSystemPrompt?: string;
|
|
13
15
|
readonly customPrompt?: string;
|
|
14
16
|
readonly os?: string;
|
|
17
|
+
readonly surface?: Surface;
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
type RunCommand = (cmd: ReadonlyArray<string>) => string | undefined;
|
|
@@ -40,7 +43,7 @@ export function buildSystemPrompt(opts: BuildOptions): string {
|
|
|
40
43
|
sections.push(
|
|
41
44
|
[
|
|
42
45
|
"<system_instructions>",
|
|
43
|
-
"You are
|
|
46
|
+
"You are Pim (Pi IMproved), a batteries-included agent built on the Pi harness.",
|
|
44
47
|
...opts.toolGuidelines.map((g) => `- ${g}`),
|
|
45
48
|
...dynamicGuidelines().map((g) => `- ${g}`),
|
|
46
49
|
"</system_instructions>",
|
|
@@ -57,6 +60,7 @@ export function buildSystemPrompt(opts: BuildOptions): string {
|
|
|
57
60
|
`- cwd: ${opts.cwd}`,
|
|
58
61
|
`- os: ${opts.os ?? describeOs()}`,
|
|
59
62
|
`- model: ${model}`,
|
|
63
|
+
...(opts.surface ? [`- surface: ${opts.surface}`] : []),
|
|
60
64
|
`- datetime: ${formatDatetime(new Date())}`,
|
|
61
65
|
"</environment>",
|
|
62
66
|
].join("\n")
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type AgentSession, loadSkills } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
5
|
|
|
3
6
|
import { loadRelative } from "./catalog";
|
|
4
7
|
import { rankCommands } from "./commandRanker";
|
|
@@ -15,6 +18,25 @@ export type PickerServiceDeps = {
|
|
|
15
18
|
|
|
16
19
|
const DEFAULT_LIMIT = 50;
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The `.agents/skills` roots pi reads and `loadSkills` does not: every one from
|
|
23
|
+
* the cwd up to the repository root, plus the user's own.
|
|
24
|
+
*/
|
|
25
|
+
function agentsSkillDirs(cwd: string): string[] {
|
|
26
|
+
const dirs: string[] = [];
|
|
27
|
+
let dir = resolve(cwd);
|
|
28
|
+
for (;;) {
|
|
29
|
+
dirs.push(join(dir, ".agents", "skills"));
|
|
30
|
+
const parent = dirname(dir);
|
|
31
|
+
if (existsSync(join(dir, ".git")) || parent === dir) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
dirs.push(join(process.env.HOME ?? homedir(), ".agents", "skills"));
|
|
37
|
+
return dirs;
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
/** Answers one session's picker queries against the machine the agent runs on. */
|
|
19
41
|
export class PickerService {
|
|
20
42
|
private readonly deps: PickerServiceDeps;
|
|
@@ -81,7 +103,7 @@ export class PickerService {
|
|
|
81
103
|
const { skills } = loadSkills({
|
|
82
104
|
cwd,
|
|
83
105
|
agentDir: this.deps.agentDir,
|
|
84
|
-
skillPaths:
|
|
106
|
+
skillPaths: agentsSkillDirs(cwd),
|
|
85
107
|
includeDefaults: true,
|
|
86
108
|
});
|
|
87
109
|
return skills.map((skill) => ({
|
|
@@ -7,12 +7,12 @@ export type PickerToken = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export const AT_PREFIX = /(?:^|\s)@(\S*)$/;
|
|
10
|
-
export const SLASH_PREFIX =
|
|
10
|
+
export const SLASH_PREFIX = /(?:^|\s)\/(\S*)$/;
|
|
11
11
|
|
|
12
|
-
/** Offset of the sigil in
|
|
12
|
+
/** Offset of the sigil in a match that also swallows the whitespace before it. */
|
|
13
13
|
export function sigilOffset(match: RegExpMatchArray): number {
|
|
14
14
|
const matched = match[0] ?? "";
|
|
15
|
-
return (match.index ?? 0) + (matched
|
|
15
|
+
return (match.index ?? 0) + (/^\s/.test(matched) ? 1 : 0);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function isDirectoryItem(item: { readonly label: string }): boolean {
|
|
@@ -28,7 +28,11 @@ export function activeToken(
|
|
|
28
28
|
|
|
29
29
|
const slash = SLASH_PREFIX.exec(beforeCaret);
|
|
30
30
|
if (slash) {
|
|
31
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
kind: "command",
|
|
33
|
+
query: slash[1] ?? "",
|
|
34
|
+
start: lineStart + sigilOffset(slash),
|
|
35
|
+
};
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
const at = AT_PREFIX.exec(beforeCaret);
|
|
@@ -59,9 +63,12 @@ export function applyCompletion(
|
|
|
59
63
|
token: PickerToken,
|
|
60
64
|
item: { readonly value: string; readonly label: string }
|
|
61
65
|
): Completion {
|
|
62
|
-
const inserted = token.kind === "file" ? `@${item.value}` : `${item.value} `;
|
|
63
66
|
const head = text.slice(0, token.start);
|
|
64
67
|
const tail = text.slice(caret);
|
|
68
|
+
const inserted =
|
|
69
|
+
token.kind === "file"
|
|
70
|
+
? `@${item.value}`
|
|
71
|
+
: `${item.value}${/^\s/.test(tail) ? "" : " "}`;
|
|
65
72
|
return {
|
|
66
73
|
text: `${head}${inserted}${tail}`,
|
|
67
74
|
caret: head.length + inserted.length,
|
|
@@ -3,8 +3,9 @@ import type { BunFile } from "bun";
|
|
|
3
3
|
import type { FileEntry, SessionHeader } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { parseSessionEntries } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { SessionDigest, type Durable } from "./SessionDigest";
|
|
7
|
+
|
|
8
|
+
export type { SessionDigest } from "./SessionDigest";
|
|
8
9
|
|
|
9
10
|
async function ifPresent<T>(read: () => Promise<T>, absent: T): Promise<T> {
|
|
10
11
|
try {
|
|
@@ -28,8 +29,6 @@ type Cursor = {
|
|
|
28
29
|
readonly seq: number;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
const PROBE_LINES = 16;
|
|
32
|
-
|
|
33
32
|
const PROBE_LIMIT = 16 * 1024 * 1024;
|
|
34
33
|
|
|
35
34
|
const PROBE_CHUNK = 8 * 1024;
|
|
@@ -37,19 +36,6 @@ const PROBE_CHUNK_MAX = 256 * 1024;
|
|
|
37
36
|
|
|
38
37
|
const EMPTY = new Uint8Array();
|
|
39
38
|
|
|
40
|
-
const TAIL_BYTES = 64 * 1024;
|
|
41
|
-
const TAIL_LINES = 10;
|
|
42
|
-
|
|
43
|
-
const TITLE_LIMIT = 120;
|
|
44
|
-
|
|
45
|
-
/** What the session catalogue shows for one file without opening a session. */
|
|
46
|
-
export type SessionDigest = {
|
|
47
|
-
/** The first user message, trimmed; absent when the session has none yet. */
|
|
48
|
-
readonly title?: string;
|
|
49
|
-
/** When the agent last wrote; it moves with an in-flight turn, so read it before starting one. */
|
|
50
|
-
readonly settledAt?: number;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
39
|
function isHeader(entry: FileEntry): entry is SessionHeader {
|
|
54
40
|
return entry.type === "session";
|
|
55
41
|
}
|
|
@@ -112,17 +98,23 @@ export class EventLog {
|
|
|
112
98
|
return this.cursor.seq;
|
|
113
99
|
}
|
|
114
100
|
|
|
115
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* The name pi's own `/name` writes: the last `session_info` entry wins, and an
|
|
103
|
+
* empty one clears it. Appended rather than replaced, so it is found by reading
|
|
104
|
+
* the whole file — the one unbounded read the catalogue makes, and the only
|
|
105
|
+
* reason `SessionDigest` takes a whole file rather than a bounded head and tail.
|
|
106
|
+
*/
|
|
107
|
+
public async name(): Promise<string | undefined> {
|
|
108
|
+
const body = await durableBytes(Bun.file(this.path));
|
|
109
|
+
return body === undefined ? undefined : SessionDigest.nameOf(body);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Title and settle time for the catalogue, without opening a session. */
|
|
116
113
|
public async digest(): Promise<SessionDigest> {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
]);
|
|
122
|
-
return {
|
|
123
|
-
...(title === undefined ? {} : { title }),
|
|
124
|
-
...(settledAt === undefined ? {} : { settledAt }),
|
|
125
|
-
};
|
|
114
|
+
const body = await durableBytes(Bun.file(this.path));
|
|
115
|
+
return body === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: SessionDigest.of(SessionDigest.partsOf(body));
|
|
126
118
|
}
|
|
127
119
|
}
|
|
128
120
|
|
|
@@ -158,56 +150,7 @@ async function* headLines(
|
|
|
158
150
|
}
|
|
159
151
|
}
|
|
160
152
|
|
|
161
|
-
async function
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
const found = lastAgentTime(lines);
|
|
165
|
-
if (found !== undefined || near === 0 || lines.length >= TAIL_LINES) {
|
|
166
|
-
return found;
|
|
167
|
-
}
|
|
168
|
-
return lastAgentTime(await tailLines(file, 0));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function tailLines(file: BunFile, from: number): Promise<string[]> {
|
|
172
|
-
const text = await ifPresent(() => file.slice(from).text(), "");
|
|
173
|
-
const lines = text.split("\n");
|
|
174
|
-
lines.pop();
|
|
175
|
-
if (from > 0) {
|
|
176
|
-
lines.shift();
|
|
177
|
-
}
|
|
178
|
-
return lines.slice(-TAIL_LINES).reverse();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function lastAgentTime(lines: readonly string[]): number | undefined {
|
|
182
|
-
for (const line of lines) {
|
|
183
|
-
const entry = parseSessionEntries(line)[0];
|
|
184
|
-
if (entry?.type !== "message") {
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
const role = entry.message.role;
|
|
188
|
-
if (role === "assistant" || role === "toolResult") {
|
|
189
|
-
const at = Date.parse(entry.timestamp);
|
|
190
|
-
return Number.isNaN(at) ? undefined : at;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function firstUserMessage(file: BunFile): Promise<string | undefined> {
|
|
197
|
-
for await (const line of headLines(file, PROBE_LINES)) {
|
|
198
|
-
const entry = parseSessionEntries(line)[0];
|
|
199
|
-
if (entry?.type !== "message" || entry.message.role !== "user") {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
const said = Attachments.parse(MessageText.textOf(entry.message.content));
|
|
203
|
-
const text =
|
|
204
|
-
said.text.trim() ||
|
|
205
|
-
said.files.map((file) => Attachments.nameOf(file.path)).join(", ");
|
|
206
|
-
if (text !== "") {
|
|
207
|
-
return text.length > TITLE_LIMIT
|
|
208
|
-
? `${text.slice(0, TITLE_LIMIT).trimEnd()}…`
|
|
209
|
-
: text;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return undefined;
|
|
153
|
+
async function durableBytes(file: BunFile): Promise<Durable | undefined> {
|
|
154
|
+
const read = await ifPresent(() => file.bytes(), EMPTY);
|
|
155
|
+
return SessionDigest.durable(read);
|
|
213
156
|
}
|