@vincenthanxiaodu/pi-web 1.202608.13 → 1.202608.14

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.
@@ -114,7 +114,7 @@
114
114
  html, body { margin: 0; height: 100%; overflow: hidden; background: var(--pi-bg); color: var(--pi-text); }
115
115
  body { min-height: 100dvh; }
116
116
  </style>
117
- <script type="module" crossorigin src="./assets/index-EKiSlX_6.js"></script>
117
+ <script type="module" crossorigin src="./assets/index-CO6A9lVF.js"></script>
118
118
  <link rel="modulepreload" crossorigin href="./assets/vendor-editor-core-CXO8gGab.js">
119
119
  <link rel="modulepreload" crossorigin href="./assets/vendor-editor-languages-CpW4sJsX.js">
120
120
  </head>
@@ -0,0 +1,167 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ /**
4
+ * Background-task runs, read from the registry the task tool leaves on disk.
5
+ *
6
+ * The tool is a pi extension that runs a shell command outside the turn that
7
+ * started it, and it reports progress to the terminal UI only. In a browser
8
+ * the work was therefore invisible: a deploy could run for ten minutes with
9
+ * nothing on screen, which is exactly when someone wants to look.
10
+ *
11
+ * Attribution is the interesting part. The tool names its directory
12
+ *
13
+ * <cwd>/.pi/tasks/<sessionId ?? "session-<pid>">-<pid>/<taskId>.json
14
+ *
15
+ * and nothing ever supplies that sessionId (BackgroundTaskContext declares it
16
+ * optional and every construction site omits it), so the name collapses to the
17
+ * pid of the *server* process. One PI WEB server hosts every session, so all
18
+ * sessions share one directory and the records carry no session field. Reading
19
+ * that directory alone would show every session the same list.
20
+ *
21
+ * So the directory supplies the state and the transcript supplies the
22
+ * ownership: a task belongs to the session whose transcript mentions its
23
+ * output path, which the tool writes into its own result when the task starts.
24
+ * That is exact, survives a restart, and needs no cooperation from the tool.
25
+ */
26
+ /** A record claiming to run whose process is gone is reported as it is, not as running. */
27
+ const TASKS_SUBDIR = join(".pi", "tasks");
28
+ /** Enough of the tail to show what a task is doing without reading a long log. */
29
+ const TAIL_BYTES = 16 * 1024;
30
+ function asString(value) {
31
+ return typeof value === "string" && value !== "" ? value : undefined;
32
+ }
33
+ function asNumber(value) {
34
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
35
+ }
36
+ /**
37
+ * Whether a process is still alive.
38
+ *
39
+ * A task killed by a machine restart keeps `status: "running"` in its file
40
+ * forever, because nothing runs to correct it. Reporting that as running would
41
+ * mean the UI shows a spinner for a task that died days ago, so a running
42
+ * record is only believed while its process exists.
43
+ */
44
+ function processAlive(pid) {
45
+ if (pid === undefined)
46
+ return false;
47
+ try {
48
+ process.kill(pid, 0);
49
+ return true;
50
+ }
51
+ catch (error) {
52
+ // EPERM means the process exists but belongs to someone else, which still
53
+ // counts as alive; only ESRCH means it is gone.
54
+ return error instanceof Error && "code" in error && error.code === "EPERM";
55
+ }
56
+ }
57
+ /** Task ids this session started, taken from the output paths in its transcript. */
58
+ export async function taskIdsForSession(transcriptPath) {
59
+ const ids = new Set();
60
+ let text;
61
+ try {
62
+ text = await readFile(transcriptPath, "utf8");
63
+ }
64
+ catch {
65
+ return ids;
66
+ }
67
+ // The tool reports "Output: .pi/tasks/<dir>/<id>.output" when a task starts,
68
+ // and that line is what lands in the transcript.
69
+ const pattern = /\.pi[/\\]tasks[/\\][^"'\s]+?[/\\]([A-Za-z0-9_-]+)\.output/g;
70
+ for (const match of text.matchAll(pattern)) {
71
+ const id = match[1];
72
+ if (id !== undefined)
73
+ ids.add(id);
74
+ }
75
+ return ids;
76
+ }
77
+ /** Every task record under a workspace, regardless of which session started it. */
78
+ export async function readTaskRecords(cwd) {
79
+ const root = join(cwd, TASKS_SUBDIR);
80
+ const records = new Map();
81
+ let dirs;
82
+ try {
83
+ dirs = (await readdir(root, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
84
+ }
85
+ catch {
86
+ return records;
87
+ }
88
+ for (const dir of dirs) {
89
+ let files;
90
+ try {
91
+ files = (await readdir(join(root, dir))).filter((name) => name.endsWith(".json"));
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ for (const file of files) {
97
+ const full = join(root, dir, file);
98
+ try {
99
+ const parsed = JSON.parse(await readFile(full, "utf8"));
100
+ if (typeof parsed !== "object" || parsed === null)
101
+ continue;
102
+ // Every field is read through asString/asNumber below, so an unknown
103
+ // record is safe to hold as one: nothing here trusts its shape.
104
+ const task = parsed;
105
+ const id = asString(task.id) ?? basename(file, ".json");
106
+ records.set(id, { task, file: full });
107
+ }
108
+ catch {
109
+ // A record being written right now is skipped, not fatal: the next
110
+ // poll picks it up.
111
+ continue;
112
+ }
113
+ }
114
+ }
115
+ return records;
116
+ }
117
+ /** The tail of a task's log, for showing what it is doing without opening a file. */
118
+ export async function readTaskOutput(cwd, taskId, maxBytes = TAIL_BYTES) {
119
+ const records = await readTaskRecords(cwd);
120
+ const record = records.get(taskId);
121
+ const outputPath = record === undefined ? undefined : asString(record.task.outputPath);
122
+ if (outputPath === undefined)
123
+ return undefined;
124
+ const absolute = outputPath.startsWith("/") ? outputPath : join(cwd, outputPath);
125
+ try {
126
+ const info = await stat(absolute);
127
+ const handle = await readFile(absolute, "utf8");
128
+ return info.size > maxBytes ? handle.slice(-maxBytes) : handle;
129
+ }
130
+ catch {
131
+ return undefined;
132
+ }
133
+ }
134
+ export async function listBackgroundTasks(cwd, transcriptPath, now = Date.now()) {
135
+ const [ids, records] = await Promise.all([taskIdsForSession(transcriptPath), readTaskRecords(cwd)]);
136
+ const tasks = [];
137
+ for (const id of ids) {
138
+ const record = records.get(id);
139
+ if (record === undefined)
140
+ continue;
141
+ const { task } = record;
142
+ const rawStatus = asString(task.status) ?? "unknown";
143
+ const pid = asNumber(task.pid);
144
+ const startedAt = asNumber(task.startTime);
145
+ const endedAt = asNumber(task.endTime);
146
+ // A "running" record whose process is gone died without being able to
147
+ // record it - a restart, an OOM kill - and is reported as lost rather than
148
+ // spinning forever.
149
+ const status = rawStatus === "running" && !processAlive(pid) ? "lost" : rawStatus;
150
+ tasks.push({
151
+ id,
152
+ name: asString(task.name) ?? id,
153
+ command: asString(task.command) ?? "",
154
+ status,
155
+ startedAt: startedAt === undefined ? undefined : new Date(startedAt).toISOString(),
156
+ endedAt: endedAt === undefined ? undefined : new Date(endedAt).toISOString(),
157
+ durationMs: startedAt === undefined ? undefined : (endedAt ?? now) - startedAt,
158
+ exitCode: asNumber(task.exitCode),
159
+ bytesWritten: asNumber(task.bytesWritten) ?? 0,
160
+ hasOutput: asString(task.outputPath) !== undefined,
161
+ });
162
+ }
163
+ // Newest first: the task someone opened the page to check is the recent one.
164
+ tasks.sort((left, right) => (right.startedAt ?? "").localeCompare(left.startedAt ?? ""));
165
+ return tasks;
166
+ }
167
+ //# sourceMappingURL=backgroundTasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backgroundTasks.js","sourceRoot":"","sources":["../../../src/server/sessions/backgroundTasks.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,7 @@ import { PendingExtensionDialogStore } from "./pendingExtensionDialogStore.js";
23
23
  import { ExtensionDialogWaiters, effectiveExtensionDialogTimeoutMs, extensionDialogCancelValue } from "./extensionDialogWaiters.js";
24
24
  import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS } from "../../config.js";
25
25
  import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
26
+ import { listBackgroundTasks, readTaskOutput } from "./backgroundTasks.js";
26
27
  import { listSubagentRuns, readSubagentRunOutput } from "./subagentRuns.js";
27
28
  import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
28
29
  import { buildTranscriptView } from "./subsessionTranscript.js";
@@ -1651,6 +1652,25 @@ export class PiSessionService {
1651
1652
  // consistent, and wrong about the disk.
1652
1653
  return listSubagentRuns(dirname(sessionFile), basename(sessionFile, ".jsonl"));
1653
1654
  }
1655
+ /**
1656
+ * Background-task runs for this session.
1657
+ *
1658
+ * Keyed on the transcript rather than the task tool's own directory: that
1659
+ * directory is named after the server process, so every session in this
1660
+ * server shares it and the records carry no session field. The transcript
1661
+ * records each task's output path when it starts, which is what makes the
1662
+ * answer per session rather than per server.
1663
+ */
1664
+ async backgroundTasks(ref) {
1665
+ const session = await this.getOrOpen(ref);
1666
+ const sessionFile = session.sessionManager.getSessionFile();
1667
+ if (sessionFile === undefined)
1668
+ return [];
1669
+ return listBackgroundTasks(ref.cwd, sessionFile);
1670
+ }
1671
+ async backgroundTaskOutput(ref, taskId) {
1672
+ return readTaskOutput(ref.cwd, taskId);
1673
+ }
1654
1674
  async commands(ref) {
1655
1675
  const session = await this.getOrOpen(ref);
1656
1676
  const commands = [...BUILTIN_COMMANDS];