@piagent/platform 1.16.2 → 1.17.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 (65) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs-site/architecture.html +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/en/index.html +4 -4
  15. package/docs-site/content/en/whats-new.html +12 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +12 -0
  18. package/docs-site/context-engine.html +1 -1
  19. package/docs-site/ecosystem.html +1 -1
  20. package/docs-site/en/architecture.html +1 -1
  21. package/docs-site/en/benchmark.html +1 -1
  22. package/docs-site/en/commands.html +1 -1
  23. package/docs-site/en/context-engine.html +1 -1
  24. package/docs-site/en/ecosystem.html +1 -1
  25. package/docs-site/en/index.html +5 -5
  26. package/docs-site/en/mcp-approval.html +1 -1
  27. package/docs-site/en/mcp-auth.html +1 -1
  28. package/docs-site/en/mcp-servers.html +1 -1
  29. package/docs-site/en/mcp.html +1 -1
  30. package/docs-site/en/permissions.html +1 -1
  31. package/docs-site/en/profiles.html +1 -1
  32. package/docs-site/en/quickstart.html +1 -1
  33. package/docs-site/en/runtime.html +1 -1
  34. package/docs-site/en/scope.html +1 -1
  35. package/docs-site/en/security.html +1 -1
  36. package/docs-site/en/team.html +1 -1
  37. package/docs-site/en/whats-new.html +13 -1
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +5 -5
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +1 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  48. package/docs-site/runtime.html +1 -1
  49. package/docs-site/scope.html +1 -1
  50. package/docs-site/security.html +1 -1
  51. package/docs-site/team.html +1 -1
  52. package/docs-site/whats-new.html +13 -1
  53. package/docs-site/windows.html +1 -1
  54. package/docs-site/workflows.html +1 -1
  55. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  56. package/package.json +1 -1
  57. package/packages/piagent-core/package.json +1 -1
  58. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-BDdLJYnZ.js → MarkdownMessage-CeQ79h30.js} +1 -1
  59. package/packages/piagent-webui/dist/client/assets/{index-DJGByyfL.js → index-BNQK7rHr.js} +11 -11
  60. package/packages/piagent-webui/dist/client/index.html +1 -1
  61. package/packages/piagent-webui/gateway/git-head.ts +52 -0
  62. package/packages/piagent-webui/gateway/session-catalog.ts +8 -2
  63. package/schemas/piagent-webui/session-catalog-v1.schema.json +3 -0
  64. package/scripts/dashboard-update-job.mjs +4 -1
  65. package/scripts/piagent-dashboard.mjs +13 -1
@@ -7,7 +7,7 @@
7
7
  <meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
8
8
  <meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
9
9
  <title>Piagent WebUI</title>
10
- <script type="module" crossorigin src="/assets/index-DJGByyfL.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-BNQK7rHr.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-Z91z6SlN.css">
12
12
  </head>
13
13
  <body>
@@ -0,0 +1,52 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export type GitHead = { name: string; detached: boolean };
5
+
6
+ const MAX_DEPTH = 40;
7
+
8
+ // The branch a project folder stands on, read from the repository's HEAD file
9
+ // rather than by running git: the catalog asks for every project each time the
10
+ // dashboard refreshes, and a file read costs nothing. A linked worktree's
11
+ // `.git` is a file naming its own git directory, which holds its own HEAD.
12
+ // Null when the folder is not inside a repository or HEAD cannot be read.
13
+ export function readGitHead(cwd: string): GitHead | null {
14
+ if (!cwd) return null;
15
+ let folder = path.resolve(cwd);
16
+ for (let depth = 0; depth < MAX_DEPTH; depth += 1) {
17
+ const dotGit = path.join(folder, ".git");
18
+ let stat: fs.Stats | null = null;
19
+ try { stat = fs.statSync(dotGit); } catch { /* keep walking up */ }
20
+ if (stat) return headAt(stat.isDirectory() ? dotGit : linkedGitDir(dotGit, folder));
21
+ const parent = path.dirname(folder);
22
+ if (parent === folder) return null;
23
+ folder = parent;
24
+ }
25
+ return null;
26
+ }
27
+
28
+ function linkedGitDir(dotGitFile: string, folder: string): string | null {
29
+ try {
30
+ const match = /^gitdir:\s*(.+?)\s*$/m.exec(fs.readFileSync(dotGitFile, "utf8").slice(0, 4096));
31
+ return match ? path.resolve(folder, match[1]) : null;
32
+ } catch { return null; }
33
+ }
34
+
35
+ function headAt(gitDir: string | null): GitHead | null {
36
+ if (!gitDir) return null;
37
+ let head: string;
38
+ try { head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").slice(0, 1024).trim(); } catch { return null; }
39
+ const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
40
+ if (ref) return { name: ref[1], detached: false };
41
+ if (/^[0-9a-f]{40,64}$/i.test(head)) return { name: head.slice(0, 7), detached: true };
42
+ return null;
43
+ }
44
+
45
+ // One read per project folder while a catalog is built.
46
+ export function gitHeadReader(): (cwd: string) => GitHead | null {
47
+ const seen = new Map<string, GitHead | null>();
48
+ return (cwd) => {
49
+ if (!seen.has(cwd)) seen.set(cwd, readGitHead(cwd));
50
+ return seen.get(cwd)!;
51
+ };
52
+ }
@@ -6,6 +6,7 @@ import { workflowCommandPattern } from "../../piagent-core/runtime/workflows/web
6
6
  import type { Catalog, SessionRow } from "../contracts/generated/session-catalog-v1.ts";
7
7
  import type { MetadataSnapshot, SessionMetadata } from "./session-metadata-store.ts";
8
8
  import { projectRefForCwd, sessionRefForPath } from "../ownership/session-refs.ts";
9
+ import { gitHeadReader, type GitHead } from "./git-head.ts";
9
10
 
10
11
  export { projectRefForCwd, sessionRefForPath } from "../ownership/session-refs.ts";
11
12
 
@@ -105,7 +106,8 @@ function revision(key: Buffer, value: unknown): string {
105
106
  }
106
107
 
107
108
  function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undefined, metadataReason: string | null,
108
- ownership?: SessionOwnerProjection, sessionOptions?: SessionOptionProjection): SessionRow {
109
+ ownership?: SessionOwnerProjection, sessionOptions?: SessionOptionProjection, gitHead?: GitHead | null): SessionRow {
110
+ const gitBranch = gitHead ? { name: display(gitHead.name, 200, "HEAD"), detached: gitHead.detached } : null;
109
111
  const projectLabel = display(info.cwd ? path.basename(info.cwd) : "", 120, "Unknown project");
110
112
  const projected = projectedSessionTitle(info);
111
113
  return {
@@ -123,6 +125,7 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
123
125
  unread: metadata?.unread ?? false,
124
126
  composerAvailable: metadata?.archived ? false : ownership?.composerAvailable ?? false,
125
127
  needsAttention: metadata?.archived ? false : ownership?.needsAttention ?? false,
128
+ ...(gitBranch ? { gitBranch } : {}),
126
129
  ...(sessionOptions?.managedHelpers ? {managedHelpers:{...sessionOptions.managedHelpers,
127
130
  active: ownership?.liveState === 'idle' ? 0 : ownership?.liveState === 'running' || ownership?.liveState === 'waiting-approval' ? sessionOptions.managedHelpers.active : null}} : {}),
128
131
  ...(sessionOptions?.managedThinkingLevels ? {managedThinkingLevels:sessionOptions.managedThinkingLevels} : {}),
@@ -138,6 +141,7 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
138
141
  : ownership?.owner ?? { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "unknown" },
139
142
  sessionRevision: revision(key, [info.path, info.modified.toISOString(), info.messageCount, info.name ?? null, metadata?.revision ?? null,
140
143
  sessionOptions?.modelLabel ?? null, sessionOptions?.thinkingLevel ?? "unknown", sessionOptions?.managedHelpers, sessionOptions?.managedThinkingLevels, sessionOptions?.managedPermission ?? null, sessionOptions?.managedPlan, sessionOptions?.managedProcess,
144
+ gitBranch,
141
145
  metadata?.archived ? "archived" : ownership ? [ownership.state, ownership.liveState, ownership.owner.kind,
142
146
  ownership.owner.ownerEpoch, ownership.owner.runtimeInstanceRef, ownership.reasonCode] : "offline"]),
143
147
  reasonCode: metadata?.archived ? metadataReason : ownership?.reasonCode ?? metadataReason
@@ -151,16 +155,18 @@ export async function buildSessionCatalog(options: {
151
155
  readMetadata?(): MetadataSnapshot;
152
156
  readOwnership?(sessionRef: string): SessionOwnerProjection;
153
157
  readSessionOptions?(info: PiSessionInfo): SessionOptionProjection;
158
+ readGitHead?(cwd: string): GitHead | null;
154
159
  limit?: number;
155
160
  }): Promise<Catalog> {
156
161
  const limit = Math.max(1, Math.min(200, options.limit ?? 200));
157
162
  try {
158
163
  const found = (await options.listSessions()).filter(isUserConversationSession);
164
+ const readHead = options.readGitHead ?? gitHeadReader();
159
165
  const metadata = options.readMetadata?.() ?? { state: "ready", revision: null, sessions: new Map(), reasonCode: null };
160
166
  const sessions = found.map((info) => {
161
167
  const sessionRef = sessionRefForPath(options.key, info.path);
162
168
  return row(options.key, info, metadata.sessions.get(sessionRef), metadata.reasonCode, options.readOwnership?.(sessionRef),
163
- options.readSessionOptions?.(info));
169
+ options.readSessionOptions?.(info), readHead(info.cwd));
164
170
  }).sort((left, right) => Number(right.pinned) - Number(left.pinned)
165
171
  || Date.parse(right.updatedAt) - Date.parse(left.updatedAt)).slice(0, limit);
166
172
  return {
@@ -99,6 +99,9 @@
99
99
  "managedProcess": { "type": "object", "additionalProperties": false, "required": ["outcome", "verified", "reviewed", "blockingOpen"], "properties": {
100
100
  "outcome": { "enum": ["no_change", "interrupted", "disputed", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"] },
101
101
  "verified": { "type": "boolean" }, "reviewed": { "type": "boolean" }, "blockingOpen": { "type": "integer", "minimum": 0, "maximum": 1000 }, "planSkipped": { "const": true } } },
102
+ "gitBranch": { "type": "object", "additionalProperties": false, "required": ["name", "detached"], "properties": {
103
+ "name": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "^[^\\u0000-\\u001F\\u007F]*$" },
104
+ "detached": { "type": "boolean" } } },
102
105
  "modelLabel": { "$ref": "common-v1.schema.json#/$defs/nullableDisplayText" },
103
106
  "thinkingLevel": { "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max", "unknown"] },
104
107
  "contextUsage": { "$ref": "#/$defs/contextUsage" },
@@ -72,6 +72,9 @@ fs.writeSync(log, "\nRestarting the dashboard on the new release\n");
72
72
  // install sits under node_modules, where Node does not strip types itself.
73
73
  // --import takes a URL: a Windows path (C:\...) reads as a URL scheme.
74
74
  const loader = pathToFileURL(path.join(packageRoot, "scripts", "register-typescript-loader.mjs")).href;
75
+ // The new dashboard checks for the next release itself: only the update step
76
+ // runs without the check.
77
+ const restartEnv = { ...process.env }; delete restartEnv.PIAGENT_NO_UPDATE_CHECK;
75
78
  const restart = spawnSync(process.execPath, ["--disable-warning=ExperimentalWarning", "--import", loader, dashboard, "restart", "--json"],
76
- { stdio: ["ignore", "ignore", log], env, timeout: 3 * 60 * 1000 });
79
+ { stdio: ["ignore", "ignore", log], env: restartEnv, timeout: 3 * 60 * 1000 });
77
80
  finish("succeeded", { installed, ...(bindingChanged ? { bindingChanged } : {}), ...(restart.status === 0 ? {} : { reason: "dashboard-restart-failed" }) });
@@ -154,6 +154,18 @@ async function waitForGatewayStopped(previous, timeoutMs = 8_000) {
154
154
  throw new Error("gateway-restart-stop-timeout\nThe previous Gateway did not stop; restart refused to reuse the old process.");
155
155
  }
156
156
 
157
+ // An update job of an earlier release restarted the dashboard with update
158
+ // checks switched off (it meant them for its own install step only): a
159
+ // dashboard it starts checks for releases again.
160
+ function serveEnvironment(home = os.homedir()) {
161
+ const env = { ...process.env };
162
+ try {
163
+ const job = JSON.parse(fs.readFileSync(path.join(home, ".pi", "piagent-update-job.json"), "utf8"));
164
+ if (job?.state === "running" && job.pid === process.ppid) delete env.PIAGENT_NO_UPDATE_CHECK;
165
+ } catch { /* no update running */ }
166
+ return env;
167
+ }
168
+
157
169
  async function ensureStarted() {
158
170
  // After an update the Gateway still running is the earlier release, with
159
171
  // its pages loaded: it is replaced rather than reused.
@@ -174,7 +186,7 @@ async function ensureStarted() {
174
186
  fileURLToPath(import.meta.url),
175
187
  "serve",
176
188
  ...(agentDir ? ["--agent-dir", agentDir] : [])
177
- ], { cwd: process.cwd(), env: { ...process.env,
189
+ ], { cwd: process.cwd(), env: { ...serveEnvironment(),
178
190
  PIAGENT_PINNED_TS_TRANSFORM_ROOT: path.join(effectiveAgentDir, "npm", "node_modules", "pi-mcp-adapter") },
179
191
  detached: true, stdio: "ignore" });
180
192
  let exit = null;