@piagent/platform 1.16.1 → 1.16.3

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 (71) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  4. package/docs/command-reference-vietnamese.md +4 -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 +19 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +19 -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 +20 -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 +20 -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/extensions/piagent-guard.ts +3 -0
  58. package/packages/piagent-core/managed/resource-loader.mjs +14 -2
  59. package/packages/piagent-core/managed/session.mjs +7 -5
  60. package/packages/piagent-core/package.json +1 -1
  61. package/packages/piagent-core/runtime/update/terminal-update-offer.d.mts +7 -0
  62. package/packages/piagent-core/runtime/update/terminal-update-offer.mjs +114 -0
  63. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-sL0LOqY0.js → MarkdownMessage-BDdLJYnZ.js} +1 -1
  64. package/packages/piagent-webui/dist/client/assets/{index-Bs8l37SR.js → index-DJGByyfL.js} +12 -12
  65. package/packages/piagent-webui/dist/client/index.html +1 -1
  66. package/packages/piagent-webui/shared/company-copy.ts +99 -0
  67. package/packages/piagent-webui/shared/process-copy.ts +64 -0
  68. package/scripts/company-terminal.mjs +104 -0
  69. package/scripts/dashboard-update-job.mjs +4 -1
  70. package/scripts/piagent-dashboard.mjs +13 -1
  71. package/scripts/piagent-studio.mjs +13 -4
@@ -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-Bs8l37SR.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-DJGByyfL.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-Z91z6SlN.css">
12
12
  </head>
13
13
  <body>
@@ -0,0 +1,99 @@
1
+ // What a company failure means for the member and what to do next, in
2
+ // Vietnamese and English: the dashboard and the company Terminal say the same.
3
+ // The kind comes from Studio's (or Agent Watch's) own code; the copy never
4
+ // names the model behind a harness role.
5
+ export type CopyLocale = "vi" | "en";
6
+ const pick = (locale: CopyLocale, [vi, en]: [string, string]) => locale === "vi" ? vi : en;
7
+
8
+ // Why the company runtime did not start (Agent Watch launch codes).
9
+ export const LAUNCH_REASONS: Record<string, [string, string]> = {
10
+ "managed-config-missing": ["Chưa nhập cấu hình công ty. Mở Agent Watch → Studio → Nhập cấu hình cho Piagent (trên Windows: Kết nối Piagent trong WSL).", "Company configuration is not imported. Open Agent Watch → Studio and import it for Piagent (on Windows: Connect Piagent in WSL)."],
11
+ "managed-keychain-approval-required": ["macOS chưa cho Agent Watch đọc key. Chọn “Luôn cho phép” khi được hỏi rồi kết nối lại.", "macOS did not let Agent Watch read the key. Choose “Always Allow” when asked, then connect again."],
12
+ "managed-key-unavailable": ["Agent Watch không đọc được key công ty trên máy này. Mở Agent Watch, dán lại mã kết nối, bấm Kết nối Piagent trong WSL rồi kết nối lại.", "Agent Watch cannot read the company key on this computer. Open Agent Watch, paste the connection code again, choose Connect Piagent in WSL, then connect again."],
13
+ "managed-profile-disconnected": ["Cấu hình Piagent đang thuộc key công ty đã ngắt kết nối. Nhập lại cấu hình cho Piagent trong Agent Watch (trên Windows: Kết nối Piagent trong WSL).", "The Piagent configuration belongs to a disconnected company key. Import it again in Agent Watch (on Windows: Connect Piagent in WSL)."],
14
+ "managed-launch-binding-changed": ["Agent Watch hoặc Piagent vừa cập nhật. Nhập lại cấu hình cho Piagent trong Agent Watch (trên Windows: Kết nối Piagent trong WSL).", "Agent Watch or Piagent changed. Import the Piagent configuration again in Agent Watch (on Windows: Connect Piagent in WSL)."],
15
+ "managed-launch-version-mismatch": ["Agent Watch đang dùng một bản Piagent khác với dashboard này. Mở Agent Watch → Studio, bấm Áp dụng cho Piagent rồi kết nối lại.", "Agent Watch points at another Piagent install than this dashboard. Open Agent Watch → Studio, apply for Piagent, then connect again."],
16
+ "managed-launch-stale-runtime": ["Phiên công ty của bản Piagent cũ chưa dừng được. Thử kết nối lại sau ít giây.", "The company runtime from the previous Piagent did not stop yet. Connect again in a few seconds."],
17
+ "managed-launch-timeout": ["Phiên công ty khởi động quá lâu. Kết nối lại.", "The company runtime took too long to start. Connect again."]
18
+ };
19
+
20
+ export type Copy = { title: [string, string]; text: [string, string] };
21
+ export const COPY: Record<string, Copy> = {
22
+ "company-quota": { title: ["Hết hạn mức token", "Token quota used up"],
23
+ text: ["Hạn mức token của key này đã hết. Chờ kỳ hạn mức mới hoặc liên hệ quản trị viên để tăng hạn mức.",
24
+ "This key's token quota is used up. Wait for the next quota period or ask an administrator to raise it."] },
25
+ "company-provider-limit": { title: ["Tài khoản model của công ty hết lượt", "Company model account limited"],
26
+ text: ["Nhà cung cấp đang giới hạn tài khoản model của công ty (hết lượt dùng hoặc quá tải). Thử lại sau; quản trị viên có thể đổi model trong Harness.",
27
+ "The provider is limiting the company's model account (out of usage or overloaded). Try again later; an administrator can switch the harness model."] },
28
+ "company-account": { title: ["Tài khoản model cần đăng nhập lại", "Model account must sign in again"],
29
+ text: ["Tài khoản model của công ty đã hết phiên đăng nhập. Quản trị viên cần đăng nhập lại trong Studio (Tài khoản AI).",
30
+ "The company's model account is signed out. An administrator has to sign it in again in Studio (AI accounts)."] },
31
+ "company-busy": { title: ["Đang quá tải", "Busy"],
32
+ text: ["Studio đang xử lý quá nhiều request của key này, hoặc tài khoản model đang bận. Chờ vài giây rồi gửi lại.",
33
+ "Studio is handling too many requests for this key, or the model account is busy. Wait a few seconds and send again."] },
34
+ "company-trial-limit": { title: ["Studio hết lượt thử nghiệm", "Studio live-test limit reached"],
35
+ text: ["Studio đã dùng hết số request thử nghiệm được cấp. Quản trị viên cần nâng trần trước khi chạy tiếp.",
36
+ "Studio has used up its live-test request ceiling. An administrator has to raise it before anything runs."] },
37
+ "company-key": { title: ["Key công ty không dùng được", "Company key unusable"],
38
+ text: ["Key công ty đã hết hạn, bị thu hồi, hoặc Agent Watch không đọc được. Nhập lại key trong Agent Watch.",
39
+ "The company key expired, was revoked, or Agent Watch cannot read it. Import the key again in Agent Watch."] },
40
+ "company-policy": { title: ["Không được phép", "Not allowed"],
41
+ text: ["Key này không được dùng model, mức suy luận hoặc độ dài trả lời đang chọn. Nếu Harness vừa đổi, gửi lại để dùng cấu hình mới; nếu vẫn lỗi, kiểm tra Harness trong Studio.",
42
+ "This key may not use this model, thinking level or answer length. If the harness just changed, send again to use the new one; otherwise check the harness in Studio."] },
43
+ "company-new-session": { title: ["Cần cuộc trò chuyện mới", "New conversation needed"],
44
+ text: ["Cuộc trò chuyện này không tiếp tục được với tài khoản model hiện tại. Nội dung cũ vẫn đọc được; tạo cuộc trò chuyện mới để làm tiếp.",
45
+ "This conversation cannot continue with the current model account. It stays readable; start a new conversation to go on."] },
46
+ "company-rejected": { title: ["Yêu cầu bị từ chối", "Request rejected"],
47
+ text: ["Model từ chối yêu cầu này, thường vì cuộc trò chuyện đã dài hơn model hiện tại nhận được. Bấm Tiếp tục: phần cũ được tóm tắt rồi mới gửi. Nếu vẫn bị từ chối, rút gọn tin nhắn hoặc tạo cuộc trò chuyện mới.",
48
+ "The model rejected this request, usually because the conversation grew longer than the current model accepts. Press Continue: the older part is summarised before sending. If it is still rejected, shorten the message or start a new conversation."] },
49
+ "company-refused": { title: ["Model từ chối yêu cầu này", "The model declined this request"],
50
+ text: ["Nhà cung cấp model từ chối yêu cầu này theo chính sách sử dụng của họ (ví dụ: in, mã hoá hoặc gửi ra ngoài thông tin bí mật). Gửi lại nguyên văn sẽ bị từ chối tiếp; hãy viết lại yêu cầu theo cách khác.",
51
+ "The model's provider declined this request under its usage policy (for example, printing, encoding or sending out secrets). The same request is declined again; ask in a different way."] },
52
+ "company-unreachable": { title: ["Lỗi kết nối", "Connection error"],
53
+ text: ["Không kết nối được tới Studio. Kiểm tra mạng hoặc VPN rồi gửi lại.", "Studio could not be reached. Check the network or VPN and send again."] },
54
+ "company-service": { title: ["Dịch vụ model lỗi", "Model service error"],
55
+ text: ["Studio đã nhận yêu cầu nhưng dịch vụ model phía sau không trả lời. Gửi lại; nếu lặp lại, báo quản trị viên kèm mã bên dưới.",
56
+ "Studio took the request but its model service did not answer. Send again; if it repeats, give an administrator the code below."] },
57
+ "company-config-changed": { title: ["Cấu hình vừa thay đổi", "Configuration changed"],
58
+ text: ["Key hoặc Harness vừa thay đổi trong Studio. Gửi lại để chạy với cấu hình mới.", "The key or harness just changed in Studio. Send again to run with the new one."] },
59
+ "company-update": { title: ["Cần cập nhật Piagent", "Piagent update needed"],
60
+ text: ["Harness dùng model mà bản Piagent này chưa biết. Cập nhật Piagent rồi gửi lại.", "The harness uses a model this Piagent does not know. Update Piagent and send again."] },
61
+ "company-tool": { title: ["Công cụ lỗi", "Tool error"],
62
+ text: ["Công cụ này không chạy được ở bước này (ví dụ subagent đã chạy đủ 8 lần cho tin nhắn này).", "This tool could not run at this step (for example, the subagent already ran 8 times for this message)."] },
63
+ "company-failed": { title: ["Lỗi chưa phân loại", "Unclassified error"],
64
+ text: ["Yêu cầu không hoàn tất. Gửi lại; nếu lặp lại, báo quản trị viên kèm mã bên dưới.",
65
+ "The request did not complete. Send again; if it repeats, give an administrator the code below."] },
66
+ };
67
+ export const OTHER_KEY: [string, string] = ["Cuộc trò chuyện này thuộc thành viên hoặc Studio khác với key đang nhập trong Agent Watch. Nội dung cũ vẫn đọc được; tạo cuộc trò chuyện mới để làm tiếp.",
68
+ "This conversation belongs to another member or Studio than the key Agent Watch holds. It stays readable; start a new conversation to go on."];
69
+
70
+ // Codes that say more than their kind: no account serves a role's model, or
71
+ // the run started without one (the subagent then cannot run at all).
72
+ export const CODE_COPY: Record<string, Copy> = {
73
+ "harness_route_unavailable": { title: ["Không có tài khoản AI cho vai trò này", "No AI account for this role"],
74
+ text: ["Không tài khoản AI nào của công ty đang phục vụ model của vai trò này cho team (tài khoản bị ngắt, tắt cho team hoặc hết lượt). Quản trị viên cần gắn hoặc bật tài khoản trong Studio (Tài khoản AI), hoặc đổi model của vai trò trong Harness.",
75
+ "No company AI account serves this role's model for the team right now (disconnected, off for the team or out of usage). An administrator has to link or enable one in Studio (AI accounts), or change the role's model in the harness."] },
76
+ "harness_profile_unavailable": { title: ["Harness không có model cho vai trò này", "Harness has no model for this role"],
77
+ text: ["Lượt này bắt đầu khi Harness của team không có model dùng được cho vai trò này. Quản trị viên kiểm tra Harness và tài khoản AI của team trong Studio, rồi gửi lại.",
78
+ "This run started while the team's harness had no usable model for this role. An administrator checks the team's harness and AI accounts in Studio; then send again."] },
79
+ };
80
+
81
+ export function launchReasonText(code: string, locale: CopyLocale): string | null {
82
+ const reason = LAUNCH_REASONS[code];
83
+ return reason ? pick(locale, reason) : null;
84
+ }
85
+
86
+ export function companyFailureText(reason: string, code: string | null, locale: CopyLocale): { title: string; text: string } | null {
87
+ const copy = (code ? CODE_COPY[code] : undefined) ?? COPY[reason] ?? (reason.startsWith("company-") ? COPY["company-failed"] : undefined);
88
+ if (!copy) return null;
89
+ return { title: pick(locale, copy.title), text: pick(locale, code === "managed-session-scope-changed" ? OTHER_KEY : copy.text) };
90
+ }
91
+
92
+ // Who failed: a harness role, never a model name.
93
+ export type FailureRole = "main" | "scout" | "research" | "verify" | "review";
94
+ export function failureRoleText(role: FailureRole, locale: CopyLocale): string {
95
+ return role === "main" ? "Main agent" : role === "scout" ? pick(locale, ["Subagent scout", "Scout subagent"])
96
+ : role === "research" ? pick(locale, ["Subagent research", "Research subagent"])
97
+ : role === "verify" ? pick(locale, ["Subagent verify", "Verify subagent"])
98
+ : pick(locale, ["Subagent review", "Review subagent"]);
99
+ }
@@ -0,0 +1,64 @@
1
+ // What a Harness step says, in Vietnamese and English: the dashboard's turn
2
+ // notes and the company Terminal read the same words.
3
+ import { failureRoleText, type CopyLocale, type FailureRole } from "./company-copy.ts";
4
+
5
+ export type ProcessCopyInput = {
6
+ phase?: string; outcome?: string; loop?: number; maxLoops?: number; verified?: boolean; reviewed?: boolean; blockingOpen?: number;
7
+ planOpen?: number; planSkipped?: boolean; verifyPolicy?: string; reviewPolicy?: string; reviewUnavailable?: string; role?: FailureRole;
8
+ disputes?: number; unchanged?: boolean;
9
+ findings?: Array<{ severity: string; file?: string; line?: number | null; issue: string }>;
10
+ claims?: Array<{ claim: string }>; issues?: Array<{ kind: string; detail: string }>;
11
+ };
12
+ const pick = (locale: CopyLocale, vi: string, en: string) => locale === "vi" ? vi : en;
13
+ const SEVERITY: Record<string, [string, string]> = { blocking: ["blocking", "blocking"], major: ["major", "major"], minor: ["minor", "minor"] };
14
+ const KIND: Record<string, [string, string]> = { wrong_premise: ["sai giả định", "wrong premise"], conflicts_with_request: ["conflict với yêu cầu", "conflicts with the request"],
15
+ ambiguous: ["chưa rõ ràng", "ambiguous"], out_of_scope: ["out of scope", "out of scope"] };
16
+ const UNAVAILABLE: Record<string, [string, string]> = { too_large: ["patch quá lớn", "the patch is too large"], not_git: ["thư mục không phải git", "not a git repository"],
17
+ limit: ["subagent đã hết lượt gọi", "the subagent ran out of calls"], failed: ["subagent lỗi", "the subagent failed"] };
18
+
19
+ // How a code-changing turn ended against the Harness workflow, in a few words.
20
+ export function outcomeText(process: ProcessCopyInput, locale: CopyLocale): string {
21
+ const parts: string[] = [];
22
+ if (process.outcome === "interrupted") return pick(locale, "Lượt bị dừng trước khi check xong", "The turn stopped before its checks finished");
23
+ // A disagreement between the main agent and a subagent is the member's to settle.
24
+ if (process.outcome === "disputed") parts.push((process.disputes ?? 1) > 1 ? pick(locale, `${process.disputes} conflict giữa main agent và subagent, bạn quyết định`, `${process.disputes} disagreements between the main agent and a subagent; your call`)
25
+ : pick(locale, "Main agent và subagent conflict, bạn quyết định", "The main agent and a subagent disagree; your call"));
26
+ if (process.unchanged) return parts.join(" · ") || pick(locale, "Không đổi code", "No code changed");
27
+ if (process.verified) parts.push(pick(locale, "Check đã pass trên code cuối", "A check passed on the final code"));
28
+ else if (process.verifyPolicy !== "off") parts.push(pick(locale, "Chưa có check pass trên code cuối", "No check passed on the final code"));
29
+ if (process.reviewed) parts.push(process.blockingOpen ? pick(locale, `Còn ${process.blockingOpen} lỗi blocking chưa fix`, `${process.blockingOpen} blocking issue(s) still open`)
30
+ : pick(locale, "Review không có lỗi blocking", "Review found no blocking issue"));
31
+ else if (process.reviewPolicy !== "off") parts.push(process.outcome === "review_unavailable" ? pick(locale, "Không chạy được review", "The review could not run")
32
+ + (process.reviewUnavailable && UNAVAILABLE[process.reviewUnavailable] ? ` (${pick(locale, ...UNAVAILABLE[process.reviewUnavailable])})` : "")
33
+ : pick(locale, "Chưa review", "Not reviewed"));
34
+ if (process.planSkipped) parts.push(pick(locale, "Đã sửa code khi chưa có plan", "Code was changed before a plan existed"));
35
+ if (process.planOpen) parts.push(pick(locale, `Plan còn ${process.planOpen} bước chưa xong`, `${process.planOpen} plan step(s) not completed`));
36
+ return parts.join(" · ") || pick(locale, "Đã đổi code", "Code changed");
37
+ }
38
+
39
+ // What a harness step (other than the final one) says, in one line.
40
+ export function headlineText(process: ProcessCopyInput, locale: CopyLocale): string {
41
+ const round = process.loop && process.maxLoops ? pick(locale, ` (vòng ${process.loop}/${process.maxLoops})`, ` (round ${process.loop}/${process.maxLoops})`) : "";
42
+ const who = failureRoleText(process.role ?? "review", locale), count = (process.findings ?? process.claims ?? process.issues ?? []).length;
43
+ switch (process.phase) {
44
+ case "plan": return pick(locale, `Harness: plan còn ${process.planOpen ?? 0} bước chưa đánh dấu xong, yêu cầu agent cập nhật`, `Harness: ${process.planOpen ?? 0} plan step(s) not marked completed; asked the agent to update the plan`);
45
+ case "verify": return pick(locale, `Harness: chưa có check nào pass trên code hiện tại, yêu cầu agent chạy check${round}`, `Harness: no check has passed on the current code; asked the agent to run checks${round}`);
46
+ case "review": return pick(locale, `Harness: review tìm thấy ${count} lỗi blocking, gửi lại agent để fix hoặc giải thích${round}`, `Harness: the review found ${count} blocking issue(s); sent back to the agent to fix or answer${round}`);
47
+ case "objection": return pick(locale, `${who} phản biện brief của main agent`, `${who} objected to the main agent's brief`);
48
+ case "answer": return pick(locale, "Harness: yêu cầu main agent nói rõ đã quyết định gì với phản biện của subagent", "Harness: asked the main agent to say what it decided about the subagent's objection");
49
+ case "claims": return pick(locale, `Harness: ${who} đánh fail ${count} claim, gửi lại main agent`, `Harness: ${who} marked ${count} claim(s) as failed; sent back to the main agent`);
50
+ case "rejudge": return process.role === "verify" ? pick(locale, `Harness: ${who} check lại các claim cùng câu trả lời của main agent`, `Harness: ${who} checks the claims again with the main agent's answer`)
51
+ : pick(locale, `Harness: main agent giải thích thay vì fix, ${who} xem xét lại lời giải thích`, `Harness: the main agent answered instead of fixing; ${who} judges the answer`);
52
+ case "dispute": return pick(locale, `Main agent và ${who} vẫn conflict sau 2 lượt, bạn quyết định`, `The main agent and the ${who} still disagree after two rounds; your call`);
53
+ default: return "";
54
+ }
55
+ }
56
+
57
+ // The items a step carries: review findings, failed claims, objections to a brief.
58
+ export type ProcessItem = { tag: string; tone: "error" | "warning"; location: string | null; text: string };
59
+ export function processItems(process: ProcessCopyInput, locale: CopyLocale): ProcessItem[] {
60
+ return (process.findings ?? []).map((f): ProcessItem => ({ tag: pick(locale, ...(SEVERITY[f.severity] ?? [f.severity, f.severity])),
61
+ tone: f.severity === "blocking" ? "error" : "warning", location: f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : null, text: f.issue }))
62
+ .concat((process.claims ?? []).map((c): ProcessItem => ({ tag: "fail", tone: "error", location: null, text: c.claim })))
63
+ .concat((process.issues ?? []).map((i): ProcessItem => ({ tag: pick(locale, ...(KIND[i.kind] ?? [i.kind, i.kind])), tone: "warning", location: null, text: i.detail })));
64
+ }
@@ -0,0 +1,104 @@
1
+ // The company Terminal (`piagent studio`) says what the dashboard says: a new
2
+ // release is asked about, Bypass can be chosen, the Harness's notes and a
3
+ // failed request read in the member's words. Loaded only in the Terminal; the
4
+ // company Gateway draws its own pages.
5
+ import fs from 'node:fs';
6
+ import { createRequire } from 'node:module';
7
+ import path from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
9
+
10
+ import { registerTerminalUpdateOffer } from '../packages/piagent-core/runtime/update/terminal-update-offer.mjs';
11
+ import { parseFailure } from '../packages/piagent-core/runtime/managed-failure.mjs';
12
+ import { companyFailureText } from '../packages/piagent-webui/shared/company-copy.ts';
13
+ import { headlineText, outcomeText, processItems } from '../packages/piagent-webui/shared/process-copy.ts';
14
+
15
+ const LOCALE = 'vi';
16
+ const PERMISSION_STATUS = 'agent-watch-permission';
17
+ const ASK = 'Hỏi trước (mặc định)', BYPASS = 'Bypass: chỉ hỏi việc bắt buộc';
18
+ const PERMISSION_TEXT = {
19
+ 'workspace-write': 'Hỏi trước: mọi lệnh cần internet đều hỏi bạn.',
20
+ 'trusted-full-access': 'Bypass: lệnh cần internet chạy luôn; vẫn hỏi trước khi đẩy hoặc gửi dữ liệu ra ngoài, chạy migration, xoá không rõ đích hoặc dùng sudo.',
21
+ };
22
+
23
+ // A harness note's details, in the shape the shared copy reads.
24
+ export function processInput(details) {
25
+ if (!details || typeof details !== 'object' || typeof details.phase !== 'string') return null;
26
+ const { policy, ...rest } = details;
27
+ return { ...rest, verifyPolicy: policy?.verify, reviewPolicy: policy?.review };
28
+ }
29
+
30
+ // The lines a harness note shows: what happened, then its findings or objections.
31
+ export function processLines(details) {
32
+ const input = processInput(details);
33
+ if (!input) return null;
34
+ const head = input.phase === 'final' ? outcomeText(input, LOCALE) : headlineText(input, LOCALE);
35
+ if (!head) return null;
36
+ const items = processItems(input, LOCALE).map((item, i) => ` ${i + 1}. [${item.tag}]${item.location ? ` ${item.location} —` : ''} ${item.text}`);
37
+ return [input.phase === 'final' ? `Tiến trình: ${head}` : head, ...items];
38
+ }
39
+
40
+ // A failed company request, as the dashboard explains it (null when it is not one).
41
+ export function failureLines(errorMessage) {
42
+ const failure = parseFailure(errorMessage);
43
+ if (!failure) return null;
44
+ const copy = companyFailureText(`company-${failure.kind}`, failure.code, LOCALE);
45
+ return copy ? `${copy.title}: ${copy.text} [${failure.code}]` : null;
46
+ }
47
+
48
+ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindingChanged = () => false }) {
49
+ const require = createRequire(path.join(sdkRoot, 'package.json'));
50
+ let tui;
51
+ const components = async () => (tui ??= await import(pathToFileURL(require.resolve('@earendil-works/pi-tui')).href));
52
+ const installed = (() => { try { return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version; } catch { return undefined; } })();
53
+
54
+ return async (pi) => {
55
+ const { Box, Text } = await components();
56
+ const box = (text, theme, color) => {
57
+ const view = new Box(1, 0, (t) => theme.bg('customMessageBg', t));
58
+ view.addChild(new Text(color ? theme.fg(color, text) : text, 0, 0));
59
+ return view;
60
+ };
61
+
62
+ registerTerminalUpdateOffer(pi, { installed, packageRoot, restart: 'piagent studio',
63
+ afterUpdate: () => bindingChanged() ? 'Bản mới đổi trình khởi chạy công ty: mở Agent Watch → Studio và nhập lại cấu hình cho Piagent trước khi mở lại.' : null });
64
+
65
+ // Ask first or Bypass, kept with the conversation like the dashboard's switch.
66
+ const showPermission = (ctx) => ctx.ui?.setStatus?.(PERMISSION_STATUS, managed()?.permission === 'trusted-full-access' ? 'Bypass' : undefined);
67
+ pi.on('session_start', async (_event, ctx) => showPermission(ctx));
68
+ pi.registerCommand('bypass', {
69
+ description: 'Bật/tắt Bypass cho cuộc trò chuyện công ty này (on, off, hoặc để trống để chọn)',
70
+ getArgumentCompletions: (prefix) => ['on', 'off', 'status'].filter((v) => v.startsWith(prefix)).map((value) => ({ value, label: value })),
71
+ handler: async (args, ctx) => {
72
+ const session = managed();
73
+ if (typeof session?.session?.managedSetPermission !== 'function') { ctx.ui.notify('Cuộc trò chuyện này chưa sẵn sàng.', 'error'); return; }
74
+ const arg = args.trim().toLowerCase();
75
+ let mode = arg === 'on' ? 'trusted-full-access' : arg === 'off' ? 'workspace-write' : null;
76
+ if (!mode && arg !== 'status') {
77
+ const choice = await ctx.ui.select(`Quyền của cuộc trò chuyện này (đang: ${session.permission === 'trusted-full-access' ? 'Bypass' : 'Hỏi trước'})`, [ASK, BYPASS]);
78
+ mode = choice === BYPASS ? 'trusted-full-access' : choice === ASK ? 'workspace-write' : null;
79
+ }
80
+ if (mode && mode !== session.permission) session.session.managedSetPermission(mode);
81
+ showPermission(ctx);
82
+ ctx.ui.notify(PERMISSION_TEXT[session.permission] ?? PERMISSION_TEXT['workspace-write'], 'info');
83
+ } });
84
+
85
+ // The Harness's notes carry English for the agent; the member reads them in Vietnamese.
86
+ pi.registerMessageRenderer('agent-watch-process', (message, _options, theme) => {
87
+ const lines = processLines(message.details);
88
+ const loud = ['dispute', 'objection'].includes(message.details?.phase) || message.details?.outcome === 'disputed';
89
+ return lines ? box(lines.join('\n'), theme, loud ? 'warning' : message.details?.outcome === 'clean' ? 'success' : 'muted') : undefined;
90
+ });
91
+ pi.registerMessageRenderer('agent-watch-review-status', (_message, _options, theme) =>
92
+ box('Review đã cũ: code đổi sau khi review. Cần review lại cho thay đổi hiện tại.', theme, 'warning'));
93
+ pi.registerMessageRenderer('agent-watch-helper-receipt', (message, _options, theme) =>
94
+ box(String(message.content ?? '').replace(/(\d[\d,]*) tokens/, '$1 token').replace('review is stale', 'review đã cũ'), theme, 'dim'));
95
+
96
+ // A failed request says what it means and what to do, as on the dashboard.
97
+ pi.on('message_end', async (event, ctx) => {
98
+ const message = event?.message;
99
+ if (message?.role !== 'assistant' || message.stopReason !== 'error') return;
100
+ const text = failureLines(message.errorMessage);
101
+ if (text) ctx.ui?.notify?.(text, 'error');
102
+ });
103
+ };
104
+ }
@@ -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;
@@ -114,6 +114,11 @@ try {
114
114
  // company Gateway uses: the WebUI shows it running here and never runs it
115
115
  // at the same time. One the WebUI is running is refused.
116
116
  const { holdManagedSession } = await import('../packages/piagent-webui/ownership/managed-terminal-lease.ts');
117
+ // The Terminal's own commands and words (updates, Bypass, Harness notes): company-terminal.mjs.
118
+ const { companyTerminalExtension } = await import('./company-terminal.mjs');
119
+ const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
120
+ const terminal = companyTerminalExtension({ managed: () => current, packageRoot, sdkRoot: config.sdk_root,
121
+ bindingChanged: () => { try { return digest(config.entrypoint) !== config.entrypoint_sha256; } catch { return true; } } });
117
122
  const factory = async ({ cwd, sessionManager }) => {
118
123
  let next;
119
124
  try { next = holdManagedSession(agentDir, sessionManager.getSessionFile()); }
@@ -122,7 +127,7 @@ try {
122
127
  // A conversation that enrolled again (key or harness changed) holds the live broker.
123
128
  if (current) { broker = current.broker; await current.session.abort(); await Promise.allSettled([...current.helpers.values()]); await broker.request('close'); current.session.dispose(); await current.boundary.dispose(); }
124
129
  current = await ManagedSession.create({ sdkRoot: config.sdk_root, cwd, origin: config.origin, broker, agentDir, sessionManager,
125
- protectedRoots: [configPath, agentDir], renewBroker: newBroker, bindingRevision });
130
+ protectedRoots: [configPath, agentDir], renewBroker: newBroker, bindingRevision, terminalExtensions: [terminal] });
126
131
  return { session: current.session, extensionsResult: current.extensionsResult, services: current.services, diagnostics: [] };
127
132
  };
128
133
  const host = await api.createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager: api.SessionManager.create(cwd, path.join(agentDir, 'sessions')) });
@@ -136,7 +141,8 @@ try {
136
141
  const last=current.session.messages.filter(message=>message.role==='assistant').at(-1);
137
142
  if (last?.stopReason === 'error') {
138
143
  // Already says who failed and why, ending in its stable [code].
139
- process.stderr.write(`${String(last.errorMessage??'Agent Watch: the request failed [managed-request-failed]')}\n`);process.exitCode = 1;
144
+ const { failureLines } = await import('./company-terminal.mjs');
145
+ process.stderr.write(`${failureLines(last.errorMessage) ?? String(last.errorMessage??'Agent Watch: the request failed [managed-request-failed]')}\n`);process.exitCode = 1;
140
146
  }
141
147
  } else {
142
148
  // The company terminal runs the pinned Pi release: no "Run pi update" notice
@@ -149,7 +155,10 @@ try {
149
155
  } catch (error) {
150
156
  // Never print JSON payloads or startup objects that can contain role tokens.
151
157
  const code = error instanceof Error && /^managed-[a-z:_-]+$/.test(error.message) ? error.message : 'managed-launch-failed';
152
- const hint = code === 'managed-session-open-elsewhere' ? 'This conversation is running in the Piagent WebUI; continue it there, or start a new one here.'
153
- : process.platform === 'linux' ? 'In Agent Watch for Windows, choose Connect Piagent in WSL again.' : 'Refresh the Studio import or check Keychain access.';
158
+ // The same words as the dashboard (shared/company-copy.ts), with the code for support.
159
+ let explained = null;
160
+ try { await import('./register-typescript-loader.mjs'); explained = (await import('../packages/piagent-webui/shared/company-copy.ts')).launchReasonText(code, 'vi'); } catch { /* the code alone */ }
161
+ const hint = explained ?? (code === 'managed-session-open-elsewhere' ? 'Cuộc trò chuyện này đang chạy trong Piagent WebUI; tiếp tục ở đó, hoặc mở cuộc trò chuyện mới ở đây.'
162
+ : process.platform === 'linux' ? 'Trong Agent Watch cho Windows, chọn lại Kết nối Piagent trong WSL.' : 'Mở Agent Watch → Studio, nhập lại cấu hình cho Piagent, hoặc kiểm tra quyền Keychain.');
154
163
  process.stderr.write(`Agent Watch: ${code}. ${hint}\n`); process.exitCode = 1;
155
164
  } finally { await gateway?.close(); if (current) await current.dispose(); else await broker?.dispose(); hold?.release(); }