@piagent/platform 1.28.2 → 1.29.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 (69) hide show
  1. package/CHANGELOG.md +34 -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/company-dev-commands.md +6 -0
  6. package/docs/distribution-standard.md +6 -6
  7. package/docs/languages.json +2 -2
  8. package/docs/operator-manual-vietnamese.md +7 -7
  9. package/docs/publishing-for-teams.md +2 -0
  10. package/docs/release-install-policy.md +2 -2
  11. package/docs/team-onboarding.md +2 -2
  12. package/docs-site/architecture.html +1 -1
  13. package/docs-site/benchmark.html +1 -1
  14. package/docs-site/commands.html +1 -1
  15. package/docs-site/content/en/index.html +4 -4
  16. package/docs-site/content/en/whats-new.html +12 -0
  17. package/docs-site/content/index.html +4 -4
  18. package/docs-site/content/whats-new.html +12 -0
  19. package/docs-site/context-engine.html +1 -1
  20. package/docs-site/ecosystem.html +1 -1
  21. package/docs-site/en/architecture.html +1 -1
  22. package/docs-site/en/benchmark.html +1 -1
  23. package/docs-site/en/commands.html +1 -1
  24. package/docs-site/en/context-engine.html +1 -1
  25. package/docs-site/en/ecosystem.html +1 -1
  26. package/docs-site/en/index.html +5 -5
  27. package/docs-site/en/mcp-approval.html +1 -1
  28. package/docs-site/en/mcp-auth.html +1 -1
  29. package/docs-site/en/mcp-servers.html +1 -1
  30. package/docs-site/en/mcp.html +1 -1
  31. package/docs-site/en/permissions.html +1 -1
  32. package/docs-site/en/profiles.html +1 -1
  33. package/docs-site/en/quickstart.html +1 -1
  34. package/docs-site/en/runtime.html +1 -1
  35. package/docs-site/en/scope.html +1 -1
  36. package/docs-site/en/security.html +1 -1
  37. package/docs-site/en/team.html +1 -1
  38. package/docs-site/en/whats-new.html +13 -1
  39. package/docs-site/en/windows.html +1 -1
  40. package/docs-site/en/workflows.html +1 -1
  41. package/docs-site/index.html +5 -5
  42. package/docs-site/mcp-approval.html +1 -1
  43. package/docs-site/mcp-auth.html +1 -1
  44. package/docs-site/mcp-servers.html +1 -1
  45. package/docs-site/mcp.html +1 -1
  46. package/docs-site/permissions.html +1 -1
  47. package/docs-site/profiles.html +1 -1
  48. package/docs-site/quickstart.html +1 -1
  49. package/docs-site/runtime.html +1 -1
  50. package/docs-site/scope.html +1 -1
  51. package/docs-site/security.html +1 -1
  52. package/docs-site/team.html +1 -1
  53. package/docs-site/whats-new.html +13 -1
  54. package/docs-site/windows.html +1 -1
  55. package/docs-site/workflows.html +1 -1
  56. package/evals/private-holdout-v1/public-exposure.v2.json +1 -1
  57. package/package.json +1 -1
  58. package/packages/piagent-core/managed/binding-wait.mjs +30 -9
  59. package/packages/piagent-core/managed/memory.mjs +3 -0
  60. package/packages/piagent-core/managed/request-stream.mjs +4 -0
  61. package/packages/piagent-core/managed/session.mjs +2 -2
  62. package/packages/piagent-core/package.json +1 -1
  63. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-BKK5r1UP.js → MarkdownMessage-3vQTaeOO.js} +1 -1
  64. package/packages/piagent-webui/dist/client/assets/{index-8YNd9KDA.js → index-Dve4GNQ7.js} +3 -3
  65. package/packages/piagent-webui/dist/client/index.html +1 -1
  66. package/packages/piagent-webui/gateway/managed-launch.ts +14 -2
  67. package/packages/piagent-webui/shared/company-copy.ts +2 -2
  68. package/scripts/company-terminal.mjs +192 -0
  69. package/scripts/piagent-studio.mjs +22 -11
@@ -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-8YNd9KDA.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-Dve4GNQ7.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-B5XOKQy-.css">
12
12
  </head>
13
13
  <body>
@@ -7,13 +7,14 @@ import { fileURLToPath } from "node:url";
7
7
  import { requestGatewayControl } from "./control-socket.ts";
8
8
  import { gatewayProfileState } from "./profile-state.ts";
9
9
  import { agentWatchDataDirectory, interopEnvironment, storedSlot } from "../../piagent-core/managed/store.mjs";
10
+ import { agentWatchRunning, awaitRebind, bindingMismatch, brokerUpdatedAfterBinding } from "../../piagent-core/managed/binding-wait.mjs";
10
11
 
11
12
  // The personal dashboard shows company sessions but never runs them: it starts
12
13
  // the pinned managed entrypoint (its own process and Gateway, `--serve`) the
13
14
  // way Agent Watch's launcher does, then relays to it. Grants stay inside that
14
15
  // process; this process only reads the public binding.
15
16
  export type ManagedLaunchStatus = { schemaVersion: 1; version: "piagent-managed-launch-status-v1"; available: boolean; model: "agent-watch-auto" };
16
- type Binding = { file: string; profile: string; node: string; entrypoint: string; broker: string; brokerSha256: string };
17
+ type Binding = { file: string; profile: string; node: string; entrypoint: string; broker: string; brokerSha256: string; config: Record<string, unknown> };
17
18
 
18
19
  const CLEAN_ENV = { PATH: "/usr/bin:/bin", LANG: "en_US.UTF-8", TERM: "xterm-256color", ...interopEnvironment() };
19
20
  const READY = /^Agent Watch Auto: ready$/m;
@@ -41,7 +42,7 @@ function binding(agentDir: string): Binding | null {
41
42
  const absolute = (entry: unknown) => typeof entry === "string" && path.isAbsolute(entry);
42
43
  if (value?.schema_version !== 1 || value.model !== "agent-watch-auto" || !HEX64.test(String(value.profile_id))
43
44
  || !absolute(value.node) || !absolute(value.entrypoint) || !absolute(value.broker) || !HEX64.test(String(value.broker_sha256))) return null;
44
- return { file, profile: value.profile_id, node: value.node, entrypoint: value.entrypoint, broker: value.broker, brokerSha256: value.broker_sha256 };
45
+ return { file, profile: value.profile_id, node: value.node, entrypoint: value.entrypoint, broker: value.broker, brokerSha256: value.broker_sha256, config: value };
45
46
  } catch { return null; }
46
47
  }
47
48
 
@@ -117,6 +118,17 @@ async function start(agentDir: string, home: string, timeoutMs: number): Promise
117
118
  await new Promise((resolve) => setTimeout(resolve, 250));
118
119
  }
119
120
  }
121
+ // Right after an Agent Watch update only the broker's hash differs until
122
+ // Watch writes the new binding (up to ~70 s after it relaunches): wait for
123
+ // it, as the entrypoint does, then start over on the new binding. The
124
+ // broker never runs while any pinned hash differs.
125
+ const pins = (next: Binding) => bindingMismatch(next.config, { self: next.entrypoint, node: next.node });
126
+ if (pins(pinned) === "broker" && brokerUpdatedAfterBinding(pinned.file, pinned.broker)) {
127
+ const rebound = await awaitRebind({ running: () => agentWatchRunning(pinned.broker),
128
+ current: () => { const next = binding(agentDir); return next && pins(next) === null ? next : null; } });
129
+ if (!rebound) throw new Error(agentWatchRunning(pinned.broker) === false ? "managed-binding-watch-not-running" : "managed-binding-update-pending");
130
+ return start(agentDir, home, timeoutMs);
131
+ }
120
132
  await authorize(pinned);
121
133
  // Startup failures are reported on stderr as `Agent Watch: managed-…`; the
122
134
  // log is private and removed once the process is ready or has exited.
@@ -12,8 +12,8 @@ export const LAUNCH_REASONS: Record<string, [string, string]> = {
12
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
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
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-binding-update-pending": ["Agent Watch đang chạy nhưng chưa cập nhật cấu hình cho bản Piagent mới. Mở Agent Watch → Studio, bấm Áp dụng cho Piagent rồi mở lại.", "Agent Watch is running but has not updated the configuration for the new Piagent yet. Open Agent Watch → Studio, apply for Piagent, then open it again."],
16
- "managed-binding-watch-not-running": ["Piagent vừa cập nhật nhưng Agent Watch không chạy nên chưa cập nhật cấu hình. Mở Agent Watch, chờ vài giây rồi mở lại.", "Piagent was updated but Agent Watch is not running, so the configuration was not updated. Open Agent Watch, wait a few seconds, then open it again."],
15
+ "managed-binding-update-pending": ["Agent Watch đang chạy nhưng chưa cập nhật cấu hình sau bản Piagent hoặc Agent Watch mới. Mở Agent Watch → Studio, bấm Áp dụng cho Piagent rồi mở lại.", "Agent Watch is running but has not updated the configuration after the Piagent or Agent Watch update yet. Open Agent Watch → Studio, apply for Piagent, then open it again."],
16
+ "managed-binding-watch-not-running": ["Piagent hoặc Agent Watch vừa cập nhật nhưng Agent Watch không chạy nên chưa cập nhật cấu hình. Mở Agent Watch, chờ vài giây rồi mở lại.", "Piagent or Agent Watch was updated but Agent Watch is not running, so the configuration was not updated. Open Agent Watch, wait a few seconds, then open it again."],
17
17
  "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."],
18
18
  "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."],
19
19
  "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."],
@@ -10,6 +10,7 @@ import { pathToFileURL } from 'node:url';
10
10
  import { registerTerminalUpdateOffer } from '../packages/piagent-core/runtime/update/terminal-update-offer.mjs';
11
11
  import { parseFailure } from '../packages/piagent-core/runtime/managed-failure.mjs';
12
12
  import { checkpointsOf } from '../packages/piagent-core/managed/checkpoints.mjs';
13
+ import { memoryOn } from '../packages/piagent-core/managed/memory.mjs';
13
14
  import { companyFailureText } from '../packages/piagent-webui/shared/company-copy.ts';
14
15
  import { headlineText, outcomeText, processItems } from '../packages/piagent-webui/shared/process-copy.ts';
15
16
  import { turnEndCopy, turnEndOf } from '../packages/piagent-webui/shared/turn-end.ts';
@@ -28,6 +29,78 @@ const PERMISSION_TEXT = {
28
29
  '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.',
29
30
  };
30
31
 
32
+ const EFFORT = { off: 'Tắt', minimal: 'Tối thiểu', low: 'Thấp', medium: 'Trung bình', high: 'Cao', xhigh: 'Rất cao', max: 'Tối đa' };
33
+ export const effortText = (level) => EFFORT[level] ?? level ?? '';
34
+
35
+ // What the team's Harness decides for this conversation, as the header and
36
+ // footer say it. `last` is the main agent's latest run (its model and
37
+ // effort); before the first run, the Harness's first main model and its effort.
38
+ export function harnessSummary(managed, { last = null, version = null } = {}) {
39
+ const manifest = managed?.manifest, config = manifest?.harness?.configuration ?? {};
40
+ const review = config.workflow?.review !== 'off' && config.review?.model_ids?.[0] || null;
41
+ const fast = managed?.session?.managedFastState?.() ?? null;
42
+ return {
43
+ team: manifest?.user?.team_name || null,
44
+ model: last?.model_id || config.main?.model_ids?.[0] || null,
45
+ effort: last?.effort || config.main?.effort || managed?.session?.state?.thinkingLevel || null,
46
+ review, permission: managed?.permission === 'trusted-full-access' ? 'Bypass' : 'Hỏi trước',
47
+ fast: fast === null ? null : fast === 'on', memory: managed ? memoryOn(managed) : false, version,
48
+ };
49
+ }
50
+ export function headerText(summary) {
51
+ return ['Piagent công ty', summary.team && `team ${summary.team}`,
52
+ summary.model && `main ${summary.model}${summary.effort ? ` (${effortText(summary.effort)})` : ''}`,
53
+ summary.review && `review ${summary.review}`, summary.permission,
54
+ summary.fast !== null && `Fast ${summary.fast ? 'bật' : 'tắt'}`, `memory ${summary.memory ? 'bật' : 'tắt'}`, summary.version].filter(Boolean).join(' · ');
55
+ }
56
+ export const HEADER_KEYS = 'Esc dừng lượt · Ctrl+C xoá ô nhập, hai lần để thoát · / lệnh · ! shell · Ctrl+O xem thêm · /help';
57
+
58
+ // Pi's own commands that the company Terminal answers itself. /share and
59
+ // /bug would send the conversation and code outside the company; the others
60
+ // pick what the team's Harness decides. Keys that do the same (Ctrl+L,
61
+ // Ctrl+P, Shift+Tab) answer the same.
62
+ const ADMIN = 'Admin đổi trong Agent Studio → Harness.';
63
+ export function companyCommandText(name, summary = {}) {
64
+ const main = summary.model ? ` (main ${summary.model}${summary.effort ? `, ${effortText(summary.effort)}` : ''})` : '';
65
+ switch (name) {
66
+ case 'share': return '/share bị tắt trong cuộc trò chuyện công ty: lệnh này đăng toàn bộ hội thoại lên GitHub Gist, ra ngoài công ty. Cần lưu lại thì dùng /export (file trên máy này).';
67
+ case 'bug': return '/bug bị tắt trong cuộc trò chuyện công ty: báo cáo gửi hội thoại và code cho nhà phát triển Pi, ra ngoài công ty. Gặp lỗi, gửi admin của team mã lỗi trong [ ] và giờ xảy ra.';
68
+ case 'login': case 'logout': return 'Cuộc trò chuyện công ty dùng tài khoản AI của team qua Agent Watch: không cần /login hay /logout. Tài khoản và API key cá nhân chỉ dùng trong pi cá nhân.';
69
+ case 'model': case 'scoped-models': return `Model do Harness của team quyết định${main}; Terminal không chọn model. ${ADMIN}`;
70
+ case 'thinking': return `Mức suy luận do Harness của team quyết định${main}. ${ADMIN}`;
71
+ case 'settings': return `Cài đặt của cuộc trò chuyện công ty (compaction, retry…) do Harness quyết định. Phím tắt: /hotkeys. ${ADMIN}`;
72
+ default: return null;
73
+ }
74
+ }
75
+ // Pi's commands in the / suggestions say what they do here. They stay listed:
76
+ // with one hidden, Enter on "/bug" would pick the nearest other match (a
77
+ // skill) and send it to the agent.
78
+ const SUGGESTION_TEXT = { share: 'Tắt trong cuộc trò chuyện công ty', bug: 'Tắt trong cuộc trò chuyện công ty',
79
+ login: 'Không dùng: tài khoản của team qua Agent Watch', logout: 'Không dùng: tài khoản của team qua Agent Watch',
80
+ model: 'Harness của team quyết định model', 'scoped-models': 'Harness của team quyết định model',
81
+ thinking: 'Harness của team quyết định mức suy luận', settings: 'Harness của team quyết định', changelog: 'Có gì mới trong Piagent' };
82
+ export function companySuggestions(suggestions) {
83
+ if (!suggestions?.items || !String(suggestions.prefix ?? '').startsWith('/')) return suggestions;
84
+ return { ...suggestions, items: suggestions.items.map((item) => {
85
+ const text = SUGGESTION_TEXT[String(item.value ?? '').replace(/^\//, '').trim()];
86
+ return text ? { ...item, description: text } : item;
87
+ }) };
88
+ }
89
+
90
+ // /changelog: the newest Piagent releases from the installed CHANGELOG.md.
91
+ export function whatsNewMarkdown(changelog, count = 3) {
92
+ const sections = String(changelog ?? '').split(/^(?=## v\d)/m).filter((part) => part.startsWith('## v')).slice(0, count);
93
+ return sections.length ? sections.map((part) => part.trim()).join('\n\n') : 'Chưa có ghi chú phát hành.';
94
+ }
95
+
96
+ // The plan as a checklist, as the dashboard's Plan card shows it.
97
+ export function planLines(plan) {
98
+ const steps = Array.isArray(plan) ? plan : [];
99
+ const mark = { completed: '☑', in_progress: '☐', deferred: '⊘', pending: '☐' };
100
+ return [`Plan · ${steps.filter((p) => p.status === 'completed').length}/${steps.length} bước xong`,
101
+ ...steps.map((p) => ` ${mark[p.status] ?? '☐'} ${p.step}${p.status === 'in_progress' ? ' ← đang làm' : p.status === 'deferred' ? ' (để sau)' : ''}`)];
102
+ }
103
+
31
104
  // A harness note's details, in the shape the shared copy reads.
32
105
  export function processInput(details) {
33
106
  if (!details || typeof details !== 'object' || typeof details.phase !== 'string') return null;
@@ -53,6 +126,93 @@ export function failureLines(errorMessage) {
53
126
  return copy ? `${copy.title}: ${copy.text} [${failure.code}]` : null;
54
127
  }
55
128
 
129
+ const tokens = (n) => n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : n < 1e6 ? `${Math.round(n / 1000)}k` : `${(n / 1e6).toFixed(1)}M`;
130
+ // Pi's footer without the USD cost (company accounts are not billed per
131
+ // conversation) and with the Harness's main model and effort on the right.
132
+ export function footerLines({ session, summary, footerData, theme, width, truncateToWidth, visibleWidth }) {
133
+ const dim = (text) => theme.fg('dim', text);
134
+ if (!session) return [truncateToWidth(dim(headerText(summary)), width, dim('...'))];
135
+ const home = process.env.HOME;
136
+ let where = session.sessionManager.getCwd();
137
+ if (home && (where === home || where.startsWith(home + '/'))) where = '~' + where.slice(home.length);
138
+ const branch = footerData?.getGitBranch?.(), name = session.sessionManager.getSessionName?.();
139
+ if (branch) where += ` (${branch})`;
140
+ if (name) where += ` • ${name}`;
141
+ const sum = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
142
+ for (const entry of session.sessionManager.getEntries()) {
143
+ const usage = entry.type === 'usage' ? entry.usage : entry.type === 'message' && ['assistant', 'toolResult'].includes(entry.message?.role) ? entry.message.usage : null;
144
+ if (usage) for (const key of Object.keys(sum)) sum[key] += Number(usage[key]) || 0;
145
+ }
146
+ const context = session.getContextUsage?.(), window = context?.contextWindow ?? session.state?.model?.contextWindow ?? 0;
147
+ const percent = context?.percent ?? 0, shown = context?.percent == null ? '?' : percent.toFixed(1);
148
+ const parts = [sum.input && `↑${tokens(sum.input)}`, sum.output && `↓${tokens(sum.output)}`, sum.cacheRead && `R${tokens(sum.cacheRead)}`, sum.cacheWrite && `W${tokens(sum.cacheWrite)}`].filter(Boolean);
149
+ const ctxText = `${shown === '?' ? '?' : shown + '%'}/${tokens(window)} (auto)`;
150
+ let left = [...parts, ctxText].join(' ');
151
+ const right = summary.model ? `main ${summary.model}${summary.effort ? ` • ${effortText(summary.effort)}` : ''}` : '';
152
+ if (visibleWidth(left) > width) left = truncateToWidth(left, width, '...');
153
+ const room = width - visibleWidth(left) - 2;
154
+ const tail = room > 0 && right ? truncateToWidth(right, room, '') : '';
155
+ const pad = tail ? ' '.repeat(Math.max(2, width - visibleWidth(left) - visibleWidth(tail))) : '';
156
+ const loud = percent > 90 ? 'error' : percent > 70 ? 'warning' : null;
157
+ const stats = loud ? dim(left.slice(0, left.length - ctxText.length)) + theme.fg(loud, left.slice(left.length - ctxText.length)) : dim(left);
158
+ const lines = [truncateToWidth(dim(where), width, dim('...')), stats + dim(pad + tail)];
159
+ const statuses = footerData?.getExtensionStatuses?.();
160
+ if (statuses?.size) lines.push(truncateToWidth([...statuses.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, text]) => String(text).replace(/\s+/g, ' ').trim()).join(' '), width, dim('...')));
161
+ return lines;
162
+ }
163
+
164
+ // The update_plan tool drawn as a checklist (set on the session's tool definition).
165
+ export async function companyToolRenderers(sdkRoot) {
166
+ const require = createRequire(path.join(sdkRoot, 'package.json'));
167
+ const { Text } = await import(pathToFileURL(require.resolve('@earendil-works/pi-tui')).href);
168
+ return { update_plan: {
169
+ renderCall: (_args, theme) => new Text(theme.fg('toolTitle', theme.bold('Plan')), 0, 0),
170
+ renderResult: (result, _options, theme) => {
171
+ const plan = result?.details?.plan;
172
+ if (!Array.isArray(plan)) return new Text(theme.fg('toolOutput', (result?.content ?? []).map((c) => c.text ?? '').join('\n')), 0, 0);
173
+ const [head, ...steps] = planLines(plan);
174
+ return new Text([theme.fg('toolOutput', head), ...steps.map((line, i) => theme.fg(plan[i].status === 'completed' ? 'success' : plan[i].status === 'in_progress' ? 'accent' : 'muted', line))].join('\n'), 0, 0);
175
+ } } };
176
+ }
177
+
178
+ // Pi's InteractiveMode for the company Terminal. Pi runs its own commands
179
+ // before an extension's (and shows its /bug hint after errors) with no hook
180
+ // for either, so the few it must not run are answered here.
181
+ export async function companyInteractiveMode(api, { sdkRoot, packageRoot, summary = () => ({}) }) {
182
+ const require = createRequire(path.join(sdkRoot, 'package.json'));
183
+ const { Markdown, Spacer } = await import(pathToFileURL(require.resolve('@earendil-works/pi-tui')).href);
184
+ const changelog = () => { try { return fs.readFileSync(path.join(packageRoot, 'CHANGELOG.md'), 'utf8'); } catch { return ''; } };
185
+ return companyMode(api.InteractiveMode, { summary, whatsNew: () => whatsNewMarkdown(changelog()), components: { Markdown, Spacer, DynamicBorder: api.DynamicBorder } });
186
+ }
187
+ export function companyMode(Base, { summary, whatsNew, components }) {
188
+ const say = (mode, name, tone = 'warning') => mode.showExtensionNotify(companyCommandText(name, summary()), tone);
189
+ return class CompanyInteractiveMode extends Base {
190
+ async handleShareCommand() { say(this, 'share'); }
191
+ async handleBugCommand() { say(this, 'bug'); }
192
+ suggestBugReport() {}
193
+ crashReportInstructions() { return 'Lần sau mở lại: piagent studio, rồi /resume. Gửi admin của team giờ xảy ra lỗi.'; }
194
+ async handleLoginCommand() { say(this, 'login', 'info'); }
195
+ async showOAuthSelector() { say(this, 'logout', 'info'); }
196
+ async handleModelCommand() { say(this, 'model', 'info'); }
197
+ showModelSelector() { say(this, 'model', 'info'); }
198
+ showModelsSelector() { say(this, 'scoped-models', 'info'); }
199
+ async cycleModel() { say(this, 'model', 'info'); }
200
+ handleThinkingCommand() { say(this, 'thinking', 'info'); }
201
+ cycleThinkingLevel() { say(this, 'thinking', 'info'); }
202
+ showSettingsSelector() { say(this, 'settings', 'info'); }
203
+ // Pi's own release notes are not Piagent's.
204
+ getChangelogForDisplay() { return undefined; }
205
+ handleChangelogCommand() {
206
+ const { Markdown, Spacer, DynamicBorder } = components;
207
+ this.chatContainer.addChild(new Spacer(1));
208
+ if (DynamicBorder) this.chatContainer.addChild(new DynamicBorder());
209
+ this.chatContainer.addChild(new Markdown(`# Có gì mới trong Piagent\n\n${whatsNew()}`, 1, 1, this.getMarkdownThemeWithSettings()));
210
+ if (DynamicBorder) this.chatContainer.addChild(new DynamicBorder());
211
+ this.ui.requestRender();
212
+ }
213
+ };
214
+ }
215
+
56
216
  export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindingChanged = () => false }) {
57
217
  const require = createRequire(path.join(sdkRoot, 'package.json'));
58
218
  let tui;
@@ -129,6 +289,38 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
129
289
  description: 'Lệnh và phím tắt của Terminal công ty (không gửi model)',
130
290
  handler: async (_args, ctx) => ctx.ui.notify(HELP_TEXT, 'info') });
131
291
 
292
+ // The company header, footer and / suggestions; Pi resets extension UI on
293
+ // every session switch, so they are set again on each session_start.
294
+ let last = null;
295
+ const summary = () => {
296
+ const grant = managed()?.grant;
297
+ if (grant?.model_id) last = { model_id: grant.model_id, effort: grant.effort };
298
+ return harnessSummary(managed(), { last, version: installed });
299
+ };
300
+ pi.on('session_start', async (_event, ctx) => {
301
+ const ui = ctx.ui;
302
+ if (!ctx.hasUI || ui?.[Symbol.for('piagent.webui.gateway-runtime-ui.v1')]) return;
303
+ const { Text, truncateToWidth, visibleWidth } = await components();
304
+ ui.setHeader?.((_tui, theme) => ({ invalidate() {},
305
+ render: (width) => new Text(`${theme.bold(theme.fg('accent', headerText(summary())))}\n${theme.fg('dim', HEADER_KEYS)}`, 1, 0).render(width) }));
306
+ ui.setFooter?.((_tui, theme, footerData) => ({ invalidate() {},
307
+ render: (width) => footerLines({ session: managed()?.session, summary: summary(), footerData, theme, width, truncateToWidth, visibleWidth }) }));
308
+ ui.addAutocompleteProvider?.((current) => ({ ...current, triggerCharacters: current.triggerCharacters,
309
+ getSuggestions: async (...args) => companySuggestions(await current.getSuggestions(...args)),
310
+ applyCompletion: (...args) => current.applyCompletion(...args),
311
+ ...(current.shouldTriggerFileCompletion && { shouldTriggerFileCompletion: (...args) => current.shouldTriggerFileCompletion(...args) }) }));
312
+ });
313
+ // While a question waits for the member, the loader says so instead of "Working".
314
+ pi.on('tool_execution_start', async (event, ctx) => {
315
+ if (event?.toolName !== 'ask_user') return;
316
+ ctx.ui?.setWorkingMessage?.('Đang chờ bạn trả lời');
317
+ ctx.ui?.setWorkingIndicator?.({ frames: ['?'] });
318
+ });
319
+ pi.on('tool_execution_end', async (event, ctx) => {
320
+ if (event?.toolName !== 'ask_user') return;
321
+ ctx.ui?.setWorkingMessage?.(); ctx.ui?.setWorkingIndicator?.();
322
+ });
323
+
132
324
  // The Harness's notes carry English for the agent; the member reads them in Vietnamese.
133
325
  pi.registerMessageRenderer('agent-watch-process', (message, _options, theme) => {
134
326
  const lines = processLines(message.details);
@@ -7,7 +7,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
  import { ManagedBrokerClient } from '../packages/piagent-core/managed/broker-client.mjs';
8
8
  import { ManagedSession } from '../packages/piagent-core/managed/session.mjs';
9
9
  import { agentWatchDataDirectory, interopEnvironment, resolveStore, storedSlot } from '../packages/piagent-core/managed/store.mjs';
10
- import { agentWatchRunning, awaitRebind, bindingMismatch, installedAfterBinding } from '../packages/piagent-core/managed/binding-wait.mjs';
10
+ import { agentWatchRunning, awaitRebind, bindingMismatch, brokerUpdatedAfterBinding, installedAfterBinding } from '../packages/piagent-core/managed/binding-wait.mjs';
11
11
 
12
12
  const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
13
13
  // Node's experimental-feature notices (type stripping, SQLite) are not
@@ -44,16 +44,19 @@ try {
44
44
  if (serve && (flags.has('--project') || flags.has('--web') || flags.has('--check') || flags.has('--prompt'))) throw Error('managed-launch-arguments');
45
45
  const running = { self: fs.realpathSync(fileURLToPath(import.meta.url)), node: fs.realpathSync(process.execPath) };
46
46
  const mismatch = bindingMismatch(config, running);
47
- // Right after a Piagent update only the launcher's hash differs until Agent
48
- // Watch writes the new binding: wait for it. Nothing runs before every
49
- // pinned path and hash matches again.
50
- if (mismatch === 'entrypoint' && installedAfterBinding(configPath, config.entrypoint)) {
51
- const watch = () => agentWatchRunning(config.broker);
47
+ // Right after a Piagent update only the launcher's hash differs, right after
48
+ // an Agent Watch update only the broker's, until Agent Watch writes the new
49
+ // binding: wait for it. Nothing runs before every pinned path and hash
50
+ // matches again.
51
+ const updated = mismatch === 'entrypoint' ? installedAfterBinding(configPath, config.entrypoint)
52
+ : mismatch === 'broker' && brokerUpdatedAfterBinding(configPath, config.broker);
53
+ if (updated) {
54
+ const watch = () => agentWatchRunning(config.broker), what = mismatch === 'broker' ? 'Agent Watch' : 'Piagent';
52
55
  const rebound = await awaitRebind({ running: watch,
53
56
  current: () => { try { const next = readConfig(); return bindingMismatch(next, running) === null ? next : null; } catch { return null; } },
54
57
  waiting: state => process.stderr.write(state === false
55
- ? 'Piagent vừa cập nhật nhưng Agent Watch không chạy. Mở Agent Watch; Piagent tự tiếp tục khi cấu hình được cập nhật… [managed-binding-wait]\n'
56
- : 'Agent Watch đang cập nhật cấu hình cho bản Piagent mới… [managed-binding-wait]\n') });
58
+ ? `${what} vừa cập nhật nhưng Agent Watch không chạy. Mở Agent Watch; Piagent tự tiếp tục khi cấu hình được cập nhật… [managed-binding-wait]\n`
59
+ : `Agent Watch đang cập nhật cấu hình cho bản ${what} mới… [managed-binding-wait]\n`) });
57
60
  if (!rebound) throw Error(watch() === false ? 'managed-binding-watch-not-running' : 'managed-binding-update-pending');
58
61
  config = rebound;
59
62
  } else if (mismatch) throw Error('managed-launch-binding-changed');
@@ -131,7 +134,8 @@ try {
131
134
  // at the same time. One the WebUI is running is refused.
132
135
  const { holdManagedSession } = await import('../packages/piagent-webui/ownership/managed-terminal-lease.ts');
133
136
  // The Terminal's own commands and words (updates, Bypass, Harness notes): company-terminal.mjs.
134
- const { companyTerminalExtension } = await import('./company-terminal.mjs');
137
+ const { companyTerminalExtension, companyToolRenderers, companyInteractiveMode, harnessSummary } = await import('./company-terminal.mjs');
138
+ const toolRenderers = await companyToolRenderers(config.sdk_root);
135
139
  const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
136
140
  const terminal = companyTerminalExtension({ managed: () => current, packageRoot, sdkRoot: config.sdk_root,
137
141
  bindingChanged: () => { try { return digest(config.entrypoint) !== config.entrypoint_sha256; } catch { return true; } } });
@@ -144,6 +148,8 @@ try {
144
148
  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(); }
145
149
  current = await ManagedSession.create({ sdkRoot: config.sdk_root, cwd, origin: config.origin, broker, agentDir, sessionManager,
146
150
  protectedRoots: [configPath, agentDir], renewBroker: newBroker, bindingRevision, terminalExtensions: [terminal] });
151
+ // The plan is drawn as a checklist here; Pi keeps this definition object.
152
+ for (const [name, draw] of Object.entries(toolRenderers)) Object.assign(current.session.getToolDefinition(name) ?? {}, draw);
147
153
  return { session: current.session, extensionsResult: current.extensionsResult, services: current.services, diagnostics: [] };
148
154
  };
149
155
  const host = await api.createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager: api.SessionManager.create(cwd, path.join(agentDir, 'sessions')) });
@@ -180,8 +186,13 @@ try {
180
186
  // Pi's exit line names `pi --session-dir …`, which would open this company
181
187
  // conversation in personal Pi; name the company Terminal instead.
182
188
  const write = process.stdout.write.bind(process.stdout), project = /^[\w./~:-]+$/.test(cwd) ? cwd : `'${cwd.replaceAll("'", "'\\''")}'`;
183
- process.stdout.write = (chunk, ...rest) => write(typeof chunk === 'string' && chunk.includes('To resume this session:') ? `Mở lại cuộc trò chuyện này: piagent studio --project ${project}, rồi /resume\n` : chunk, ...rest);
184
- await new api.InteractiveMode(host, { verbose: false }).run();
189
+ // Pi's "Model scope: agent-watch-auto (Ctrl+P to cycle)" line names a
190
+ // model nobody picks; the company header says what the Harness runs.
191
+ process.stdout.write = (chunk, ...rest) => typeof chunk === 'string' && chunk.includes('Model scope: ') ? true
192
+ : write(typeof chunk === 'string' && chunk.includes('To resume this session:') ? `Mở lại cuộc trò chuyện này: piagent studio --project ${project}, rồi /resume\n` : chunk, ...rest);
193
+ // /share, /bug, /login, /model, /thinking, /settings and /changelog: company-terminal.mjs.
194
+ const CompanyMode = await companyInteractiveMode(api, { sdkRoot: config.sdk_root, packageRoot, summary: () => harnessSummary(current) });
195
+ await new CompanyMode(host, { verbose: false }).run();
185
196
  }
186
197
  }
187
198
  } catch (error) {