@piagent/platform 1.28.3 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/docs/command-reference-vietnamese.md +4 -2
- package/docs/company-dev-commands.md +6 -0
- package/docs/distribution-standard.md +6 -6
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +2 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/index.html +4 -4
- package/docs-site/content/en/whats-new.html +12 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +12 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +5 -5
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +13 -1
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +5 -5
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +13 -1
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/managed/compressed-fetch.mjs +28 -0
- package/packages/piagent-core/managed/memory.mjs +3 -0
- package/packages/piagent-core/managed/request-stream.mjs +6 -1
- package/packages/piagent-core/managed/session.mjs +2 -2
- package/packages/piagent-core/package.json +1 -1
- package/scripts/company-terminal.mjs +244 -4
- package/scripts/piagent-studio.mjs +13 -3
|
@@ -5,16 +5,57 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
8
9
|
import { pathToFileURL } from 'node:url';
|
|
9
10
|
|
|
10
11
|
import { registerTerminalUpdateOffer } from '../packages/piagent-core/runtime/update/terminal-update-offer.mjs';
|
|
11
12
|
import { parseFailure } from '../packages/piagent-core/runtime/managed-failure.mjs';
|
|
12
13
|
import { checkpointsOf } from '../packages/piagent-core/managed/checkpoints.mjs';
|
|
14
|
+
import { memoryOn } from '../packages/piagent-core/managed/memory.mjs';
|
|
13
15
|
import { companyFailureText } from '../packages/piagent-webui/shared/company-copy.ts';
|
|
14
16
|
import { headlineText, outcomeText, processItems } from '../packages/piagent-webui/shared/process-copy.ts';
|
|
15
17
|
import { turnEndCopy, turnEndOf } from '../packages/piagent-webui/shared/turn-end.ts';
|
|
16
18
|
|
|
17
19
|
const LOCALE = 'vi';
|
|
20
|
+
|
|
21
|
+
const WORKING = 'Đang làm…';
|
|
22
|
+
|
|
23
|
+
// A typed value that is already complete offers nothing, so the first Enter runs the command.
|
|
24
|
+
export const argumentCompletions = (values) => (prefix) => {
|
|
25
|
+
const typed = String(prefix ?? '').trim();
|
|
26
|
+
return values.includes(typed) ? [] : values.filter((v) => v.startsWith(typed)).map((value) => ({ value, label: value }));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const compactTokens = (n) => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
|
|
30
|
+
const textOf = (content) => typeof content === 'string' ? content : Array.isArray(content) ? content.filter((p) => p?.type === 'text').map((p) => p.text ?? '').join('\n') : '';
|
|
31
|
+
|
|
32
|
+
// Model requests, tokens, time and output speed of the member's last message,
|
|
33
|
+
// as the dashboard writes them under a turn.
|
|
34
|
+
export function turnStats(messages, now = Date.now()) {
|
|
35
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
36
|
+
const start = list.findLastIndex((m) => m?.role === 'user');
|
|
37
|
+
if (start < 0) return null;
|
|
38
|
+
const answers = list.slice(start + 1).filter((m) => m?.role === 'assistant');
|
|
39
|
+
if (!answers.length) return null;
|
|
40
|
+
const tokens = answers.reduce((n, m) => n + (m.usage?.totalTokens ?? 0), 0), output = answers.reduce((n, m) => n + (m.usage?.output ?? 0), 0);
|
|
41
|
+
const began = Number(list[start].timestamp) || null, seconds = began ? Math.max(0, Math.round((now - began) / 1000)) : null;
|
|
42
|
+
const parts = [`${answers.length} request`];
|
|
43
|
+
if (tokens) parts.push(`${compactTokens(tokens)} token`);
|
|
44
|
+
if (seconds) parts.push(seconds < 60 ? `${seconds} giây` : `${Math.floor(seconds / 60)} phút ${seconds % 60} giây`);
|
|
45
|
+
if (seconds && output) parts.push(`${Math.round(output / seconds)} tok/s`);
|
|
46
|
+
return parts.join(' · ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Team memory results stay short in the transcript (Ctrl+O shows all).
|
|
50
|
+
export function memoryResultLines(text, expanded = false) {
|
|
51
|
+
const lines = String(text ?? '').split('\n').map((l) => l.trimEnd()).filter((l) => l.trim());
|
|
52
|
+
if (expanded || lines.length <= 4) return lines;
|
|
53
|
+
const facts = lines.filter((l) => /^\s*-\s/.test(l)).length;
|
|
54
|
+
return [...lines.slice(0, 3), `… ${facts ? `${facts} ghi chú, ` : ''}${lines.length - 3} dòng nữa (Ctrl+O để xem hết)`];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const branchOf = (cwd) => new Promise((resolve) => execFile('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], { timeout: 3000 },
|
|
58
|
+
(error, out) => resolve(error ? null : String(out).trim() || null)));
|
|
18
59
|
export const HELP_TEXT = [
|
|
19
60
|
'Cuộc trò chuyện công ty: /undo hoàn tác file lượt trước đã sửa · /bypass on|off|status quyền lệnh cần internet · /fast on|off|status (khi Harness cho phép) · /skill:<tên> hoặc /<lệnh> của bạn và của project.',
|
|
20
61
|
'Pi: /session thông tin cuộc trò chuyện · /resume mở cuộc trò chuyện công ty khác · /new cuộc mới · /compact nén ngữ cảnh · /tree, /fork nhánh · /name đặt tên · /copy, /export · /hotkeys toàn bộ phím tắt · /quit thoát.',
|
|
@@ -28,6 +69,78 @@ const PERMISSION_TEXT = {
|
|
|
28
69
|
'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
70
|
};
|
|
30
71
|
|
|
72
|
+
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' };
|
|
73
|
+
export const effortText = (level) => EFFORT[level] ?? level ?? '';
|
|
74
|
+
|
|
75
|
+
// What the team's Harness decides for this conversation, as the header and
|
|
76
|
+
// footer say it. `last` is the main agent's latest run (its model and
|
|
77
|
+
// effort); before the first run, the Harness's first main model and its effort.
|
|
78
|
+
export function harnessSummary(managed, { last = null, version = null } = {}) {
|
|
79
|
+
const manifest = managed?.manifest, config = manifest?.harness?.configuration ?? {};
|
|
80
|
+
const review = config.workflow?.review !== 'off' && config.review?.model_ids?.[0] || null;
|
|
81
|
+
const fast = managed?.session?.managedFastState?.() ?? null;
|
|
82
|
+
return {
|
|
83
|
+
team: manifest?.user?.team_name || null,
|
|
84
|
+
model: last?.model_id || config.main?.model_ids?.[0] || null,
|
|
85
|
+
effort: last?.effort || config.main?.effort || managed?.session?.state?.thinkingLevel || null,
|
|
86
|
+
review, permission: managed?.permission === 'trusted-full-access' ? 'Bypass' : 'Hỏi trước',
|
|
87
|
+
fast: fast === null ? null : fast === 'on', memory: managed ? memoryOn(managed) : false, version,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function headerText(summary) {
|
|
91
|
+
return ['Piagent công ty', summary.team && `team ${summary.team}`,
|
|
92
|
+
summary.model && `main ${summary.model}${summary.effort ? ` (${effortText(summary.effort)})` : ''}`,
|
|
93
|
+
summary.review && `review ${summary.review}`, summary.permission,
|
|
94
|
+
summary.fast !== null && `Fast ${summary.fast ? 'bật' : 'tắt'}`, `memory ${summary.memory ? 'bật' : 'tắt'}`, summary.version].filter(Boolean).join(' · ');
|
|
95
|
+
}
|
|
96
|
+
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';
|
|
97
|
+
|
|
98
|
+
// Pi's own commands that the company Terminal answers itself. /share and
|
|
99
|
+
// /bug would send the conversation and code outside the company; the others
|
|
100
|
+
// pick what the team's Harness decides. Keys that do the same (Ctrl+L,
|
|
101
|
+
// Ctrl+P, Shift+Tab) answer the same.
|
|
102
|
+
const ADMIN = 'Admin đổi trong Agent Studio → Harness.';
|
|
103
|
+
export function companyCommandText(name, summary = {}) {
|
|
104
|
+
const main = summary.model ? ` (main ${summary.model}${summary.effort ? `, ${effortText(summary.effort)}` : ''})` : '';
|
|
105
|
+
switch (name) {
|
|
106
|
+
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).';
|
|
107
|
+
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.';
|
|
108
|
+
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.';
|
|
109
|
+
case 'model': case 'scoped-models': return `Model do Harness của team quyết định${main}; Terminal không chọn model. ${ADMIN}`;
|
|
110
|
+
case 'thinking': return `Mức suy luận do Harness của team quyết định${main}. ${ADMIN}`;
|
|
111
|
+
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}`;
|
|
112
|
+
default: return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Pi's commands in the / suggestions say what they do here. They stay listed:
|
|
116
|
+
// with one hidden, Enter on "/bug" would pick the nearest other match (a
|
|
117
|
+
// skill) and send it to the agent.
|
|
118
|
+
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',
|
|
119
|
+
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',
|
|
120
|
+
model: 'Harness của team quyết định model', 'scoped-models': 'Harness của team quyết định model',
|
|
121
|
+
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' };
|
|
122
|
+
export function companySuggestions(suggestions) {
|
|
123
|
+
if (!suggestions?.items || !String(suggestions.prefix ?? '').startsWith('/')) return suggestions;
|
|
124
|
+
return { ...suggestions, items: suggestions.items.map((item) => {
|
|
125
|
+
const text = SUGGESTION_TEXT[String(item.value ?? '').replace(/^\//, '').trim()];
|
|
126
|
+
return text ? { ...item, description: text } : item;
|
|
127
|
+
}) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// /changelog: the newest Piagent releases from the installed CHANGELOG.md.
|
|
131
|
+
export function whatsNewMarkdown(changelog, count = 3) {
|
|
132
|
+
const sections = String(changelog ?? '').split(/^(?=## v\d)/m).filter((part) => part.startsWith('## v')).slice(0, count);
|
|
133
|
+
return sections.length ? sections.map((part) => part.trim()).join('\n\n') : 'Chưa có ghi chú phát hành.';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The plan as a checklist, as the dashboard's Plan card shows it.
|
|
137
|
+
export function planLines(plan) {
|
|
138
|
+
const steps = Array.isArray(plan) ? plan : [];
|
|
139
|
+
const mark = { completed: '☑', in_progress: '☐', deferred: '⊘', pending: '☐' };
|
|
140
|
+
return [`Plan · ${steps.filter((p) => p.status === 'completed').length}/${steps.length} bước xong`,
|
|
141
|
+
...steps.map((p) => ` ${mark[p.status] ?? '☐'} ${p.step}${p.status === 'in_progress' ? ' ← đang làm' : p.status === 'deferred' ? ' (để sau)' : ''}`)];
|
|
142
|
+
}
|
|
143
|
+
|
|
31
144
|
// A harness note's details, in the shape the shared copy reads.
|
|
32
145
|
export function processInput(details) {
|
|
33
146
|
if (!details || typeof details !== 'object' || typeof details.phase !== 'string') return null;
|
|
@@ -53,6 +166,96 @@ export function failureLines(errorMessage) {
|
|
|
53
166
|
return copy ? `${copy.title}: ${copy.text} [${failure.code}]` : null;
|
|
54
167
|
}
|
|
55
168
|
|
|
169
|
+
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`;
|
|
170
|
+
// Pi's footer without the USD cost (company accounts are not billed per
|
|
171
|
+
// conversation) and with the Harness's main model and effort on the right.
|
|
172
|
+
export function footerLines({ session, summary, footerData, theme, width, truncateToWidth, visibleWidth }) {
|
|
173
|
+
const dim = (text) => theme.fg('dim', text);
|
|
174
|
+
if (!session) return [truncateToWidth(dim(headerText(summary)), width, dim('...'))];
|
|
175
|
+
const home = process.env.HOME;
|
|
176
|
+
let where = session.sessionManager.getCwd();
|
|
177
|
+
if (home && (where === home || where.startsWith(home + '/'))) where = '~' + where.slice(home.length);
|
|
178
|
+
const branch = footerData?.getGitBranch?.(), name = session.sessionManager.getSessionName?.();
|
|
179
|
+
if (branch) where += ` (${branch})`;
|
|
180
|
+
if (name) where += ` • ${name}`;
|
|
181
|
+
const sum = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
182
|
+
for (const entry of session.sessionManager.getEntries()) {
|
|
183
|
+
const usage = entry.type === 'usage' ? entry.usage : entry.type === 'message' && ['assistant', 'toolResult'].includes(entry.message?.role) ? entry.message.usage : null;
|
|
184
|
+
if (usage) for (const key of Object.keys(sum)) sum[key] += Number(usage[key]) || 0;
|
|
185
|
+
}
|
|
186
|
+
const context = session.getContextUsage?.(), window = context?.contextWindow ?? session.state?.model?.contextWindow ?? 0;
|
|
187
|
+
const percent = context?.percent ?? 0, shown = context?.percent == null ? '?' : percent.toFixed(1);
|
|
188
|
+
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);
|
|
189
|
+
const ctxText = `${shown === '?' ? '?' : shown + '%'}/${tokens(window)} (auto)`;
|
|
190
|
+
let left = [...parts, ctxText].join(' ');
|
|
191
|
+
const right = summary.model ? `main ${summary.model}${summary.effort ? ` • ${effortText(summary.effort)}` : ''}` : '';
|
|
192
|
+
if (visibleWidth(left) > width) left = truncateToWidth(left, width, '...');
|
|
193
|
+
const room = width - visibleWidth(left) - 2;
|
|
194
|
+
const tail = room > 0 && right ? truncateToWidth(right, room, '') : '';
|
|
195
|
+
const pad = tail ? ' '.repeat(Math.max(2, width - visibleWidth(left) - visibleWidth(tail))) : '';
|
|
196
|
+
const loud = percent > 90 ? 'error' : percent > 70 ? 'warning' : null;
|
|
197
|
+
const stats = loud ? dim(left.slice(0, left.length - ctxText.length)) + theme.fg(loud, left.slice(left.length - ctxText.length)) : dim(left);
|
|
198
|
+
const lines = [truncateToWidth(dim(where), width, dim('...')), stats + dim(pad + tail)];
|
|
199
|
+
const statuses = footerData?.getExtensionStatuses?.();
|
|
200
|
+
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('...')));
|
|
201
|
+
return lines;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// The update_plan tool drawn as a checklist (set on the session's tool definition).
|
|
205
|
+
export async function companyToolRenderers(sdkRoot) {
|
|
206
|
+
const require = createRequire(path.join(sdkRoot, 'package.json'));
|
|
207
|
+
const { Text } = await import(pathToFileURL(require.resolve('@earendil-works/pi-tui')).href);
|
|
208
|
+
const memory = (title) => ({
|
|
209
|
+
renderCall: (args, theme) => new Text(theme.fg('toolTitle', theme.bold(title)) + (args?.query || args?.name ? theme.fg('muted', ` ${args.query ?? args.name}`) : ''), 0, 0),
|
|
210
|
+
renderResult: (result, options, theme) => new Text(theme.fg('toolOutput', memoryResultLines((result?.content ?? []).map((c) => c.text ?? '').join('\n'), options?.expanded).join('\n')), 0, 0) });
|
|
211
|
+
return { memory_search: memory('Team memory'), memory_page: memory('Trang kiến thức'), update_plan: {
|
|
212
|
+
renderCall: (_args, theme) => new Text(theme.fg('toolTitle', theme.bold('Plan')), 0, 0),
|
|
213
|
+
renderResult: (result, _options, theme) => {
|
|
214
|
+
const plan = result?.details?.plan;
|
|
215
|
+
if (!Array.isArray(plan)) return new Text(theme.fg('toolOutput', (result?.content ?? []).map((c) => c.text ?? '').join('\n')), 0, 0);
|
|
216
|
+
const [head, ...steps] = planLines(plan);
|
|
217
|
+
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);
|
|
218
|
+
} } };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Pi's InteractiveMode for the company Terminal. Pi runs its own commands
|
|
222
|
+
// before an extension's (and shows its /bug hint after errors) with no hook
|
|
223
|
+
// for either, so the few it must not run are answered here.
|
|
224
|
+
export async function companyInteractiveMode(api, { sdkRoot, packageRoot, summary = () => ({}) }) {
|
|
225
|
+
const require = createRequire(path.join(sdkRoot, 'package.json'));
|
|
226
|
+
const { Markdown, Spacer } = await import(pathToFileURL(require.resolve('@earendil-works/pi-tui')).href);
|
|
227
|
+
const changelog = () => { try { return fs.readFileSync(path.join(packageRoot, 'CHANGELOG.md'), 'utf8'); } catch { return ''; } };
|
|
228
|
+
return companyMode(api.InteractiveMode, { summary, whatsNew: () => whatsNewMarkdown(changelog()), components: { Markdown, Spacer, DynamicBorder: api.DynamicBorder } });
|
|
229
|
+
}
|
|
230
|
+
export function companyMode(Base, { summary, whatsNew, components }) {
|
|
231
|
+
const say = (mode, name, tone = 'warning') => mode.showExtensionNotify(companyCommandText(name, summary()), tone);
|
|
232
|
+
return class CompanyInteractiveMode extends Base {
|
|
233
|
+
async handleShareCommand() { say(this, 'share'); }
|
|
234
|
+
async handleBugCommand() { say(this, 'bug'); }
|
|
235
|
+
suggestBugReport() {}
|
|
236
|
+
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.'; }
|
|
237
|
+
async handleLoginCommand() { say(this, 'login', 'info'); }
|
|
238
|
+
async showOAuthSelector() { say(this, 'logout', 'info'); }
|
|
239
|
+
async handleModelCommand() { say(this, 'model', 'info'); }
|
|
240
|
+
showModelSelector() { say(this, 'model', 'info'); }
|
|
241
|
+
showModelsSelector() { say(this, 'scoped-models', 'info'); }
|
|
242
|
+
async cycleModel() { say(this, 'model', 'info'); }
|
|
243
|
+
handleThinkingCommand() { say(this, 'thinking', 'info'); }
|
|
244
|
+
cycleThinkingLevel() { say(this, 'thinking', 'info'); }
|
|
245
|
+
showSettingsSelector() { say(this, 'settings', 'info'); }
|
|
246
|
+
// Pi's own release notes are not Piagent's.
|
|
247
|
+
getChangelogForDisplay() { return undefined; }
|
|
248
|
+
handleChangelogCommand() {
|
|
249
|
+
const { Markdown, Spacer, DynamicBorder } = components;
|
|
250
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
251
|
+
if (DynamicBorder) this.chatContainer.addChild(new DynamicBorder());
|
|
252
|
+
this.chatContainer.addChild(new Markdown(`# Có gì mới trong Piagent\n\n${whatsNew()}`, 1, 1, this.getMarkdownThemeWithSettings()));
|
|
253
|
+
if (DynamicBorder) this.chatContainer.addChild(new DynamicBorder());
|
|
254
|
+
this.ui.requestRender();
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
56
259
|
export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindingChanged = () => false }) {
|
|
57
260
|
const require = createRequire(path.join(sdkRoot, 'package.json'));
|
|
58
261
|
let tui;
|
|
@@ -75,7 +278,7 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
75
278
|
pi.on('session_start', async (_event, ctx) => showPermission(ctx));
|
|
76
279
|
pi.registerCommand('bypass', {
|
|
77
280
|
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)',
|
|
78
|
-
getArgumentCompletions: (
|
|
281
|
+
getArgumentCompletions: argumentCompletions(['on', 'off', 'status']),
|
|
79
282
|
handler: async (args, ctx) => {
|
|
80
283
|
const session = managed();
|
|
81
284
|
if (typeof session?.session?.managedSetPermission !== 'function') { ctx.ui.notify('Cuộc trò chuyện này chưa sẵn sàng.', 'error'); return; }
|
|
@@ -106,7 +309,7 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
106
309
|
pi.on('session_start', async (_event, ctx) => showFast(ctx));
|
|
107
310
|
pi.registerCommand('fast', {
|
|
108
311
|
description: 'Bật/tắt Fast cho cuộc trò chuyện công ty này (on, off, status); chỉ có khi Harness của team cho phép',
|
|
109
|
-
getArgumentCompletions: (
|
|
312
|
+
getArgumentCompletions: argumentCompletions(['on', 'off', 'status']),
|
|
110
313
|
handler: async (args, ctx) => {
|
|
111
314
|
const session = managed()?.session, state = session?.managedFastState?.();
|
|
112
315
|
if (typeof session?.managedSetFast !== 'function') { ctx.ui.notify('Cuộc trò chuyện này chưa sẵn sàng.', 'error'); return; }
|
|
@@ -129,6 +332,38 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
129
332
|
description: 'Lệnh và phím tắt của Terminal công ty (không gửi model)',
|
|
130
333
|
handler: async (_args, ctx) => ctx.ui.notify(HELP_TEXT, 'info') });
|
|
131
334
|
|
|
335
|
+
// The company header, footer and / suggestions; Pi resets extension UI on
|
|
336
|
+
// every session switch, so they are set again on each session_start.
|
|
337
|
+
let last = null;
|
|
338
|
+
const summary = () => {
|
|
339
|
+
const grant = managed()?.grant;
|
|
340
|
+
if (grant?.model_id) last = { model_id: grant.model_id, effort: grant.effort };
|
|
341
|
+
return harnessSummary(managed(), { last, version: installed });
|
|
342
|
+
};
|
|
343
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
344
|
+
const ui = ctx.ui;
|
|
345
|
+
if (!ctx.hasUI || ui?.[Symbol.for('piagent.webui.gateway-runtime-ui.v1')]) return;
|
|
346
|
+
const { Text, truncateToWidth, visibleWidth } = await components();
|
|
347
|
+
ui.setHeader?.((_tui, theme) => ({ invalidate() {},
|
|
348
|
+
render: (width) => new Text(`${theme.bold(theme.fg('accent', headerText(summary())))}\n${theme.fg('dim', HEADER_KEYS)}`, 1, 0).render(width) }));
|
|
349
|
+
ui.setFooter?.((_tui, theme, footerData) => ({ invalidate() {},
|
|
350
|
+
render: (width) => footerLines({ session: managed()?.session, summary: summary(), footerData, theme, width, truncateToWidth, visibleWidth }) }));
|
|
351
|
+
ui.addAutocompleteProvider?.((current) => ({ ...current, triggerCharacters: current.triggerCharacters,
|
|
352
|
+
getSuggestions: async (...args) => companySuggestions(await current.getSuggestions(...args)),
|
|
353
|
+
applyCompletion: (...args) => current.applyCompletion(...args),
|
|
354
|
+
...(current.shouldTriggerFileCompletion && { shouldTriggerFileCompletion: (...args) => current.shouldTriggerFileCompletion(...args) }) }));
|
|
355
|
+
});
|
|
356
|
+
// While a question waits for the member, the loader says so instead of "Working".
|
|
357
|
+
pi.on('tool_execution_start', async (event, ctx) => {
|
|
358
|
+
if (event?.toolName !== 'ask_user') return;
|
|
359
|
+
ctx.ui?.setWorkingMessage?.('Đang chờ bạn trả lời');
|
|
360
|
+
ctx.ui?.setWorkingIndicator?.({ frames: ['?'] });
|
|
361
|
+
});
|
|
362
|
+
pi.on('tool_execution_end', async (event, ctx) => {
|
|
363
|
+
if (event?.toolName !== 'ask_user') return;
|
|
364
|
+
ctx.ui?.setWorkingMessage?.(WORKING); ctx.ui?.setWorkingIndicator?.();
|
|
365
|
+
});
|
|
366
|
+
|
|
132
367
|
// The Harness's notes carry English for the agent; the member reads them in Vietnamese.
|
|
133
368
|
pi.registerMessageRenderer('agent-watch-process', (message, _options, theme) => {
|
|
134
369
|
const lines = processLines(message.details);
|
|
@@ -142,11 +377,16 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
142
377
|
const session = managed()?.session;
|
|
143
378
|
if (!session?.subscribe || heard === session) return;
|
|
144
379
|
heard = session;
|
|
380
|
+
const showBranch = async () => { const branch = await branchOf(managed()?.cwd ?? ctx.cwd); ctx.ui?.setStatus?.('agent-watch-branch', branch ? `⎇ ${branch}` : undefined); };
|
|
381
|
+
ctx.ui?.setWorkingMessage?.(WORKING);
|
|
382
|
+
void showBranch();
|
|
145
383
|
session.subscribe((event) => {
|
|
384
|
+
if (event?.type === 'managed_helpers') ctx.ui?.setStatus?.('agent-watch-helpers', event.active ? `Subagent ${event.active}/${event.maximum}` : undefined);
|
|
146
385
|
const end = event?.type === 'managed_turn_end' ? turnEndOf(event.end) : null;
|
|
147
386
|
if (!end) return;
|
|
148
|
-
const copy = turnEndCopy(end, LOCALE);
|
|
149
|
-
ctx.ui?.notify?.(`${copy.title}. ${copy.text.replace('Bấm Tiếp tục', 'Gửi "tiếp tục"').replace('bấm Tiếp tục', 'gửi "tiếp tục"')}`, copy.tone === 'success' ? 'info' : copy.tone === 'error' ? 'error' : copy.tone === 'warning' ? 'warning' : 'info');
|
|
387
|
+
const copy = turnEndCopy(end, LOCALE), stats = turnStats(session.messages);
|
|
388
|
+
ctx.ui?.notify?.(`${copy.title}. ${copy.text.replace('Bấm Tiếp tục', 'Gửi "tiếp tục"').replace('bấm Tiếp tục', 'gửi "tiếp tục"')}${stats ? `\n${stats}` : ''}`, copy.tone === 'success' ? 'info' : copy.tone === 'error' ? 'error' : copy.tone === 'warning' ? 'warning' : 'info');
|
|
389
|
+
void showBranch();
|
|
150
390
|
});
|
|
151
391
|
});
|
|
152
392
|
pi.registerMessageRenderer('agent-watch-review-status', (_message, _options, theme) =>
|
|
@@ -23,6 +23,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
23
23
|
}
|
|
24
24
|
if (flags.has('--json') && !flags.has('--prompt')) throw Error('managed-launch-arguments');
|
|
25
25
|
const home = os.homedir();
|
|
26
|
+
// Joining Agent Watch takes a few seconds; keys typed before the Terminal draws are lost, so say so.
|
|
27
|
+
if (!['--check', '--prompt', '--serve', '--doctor', '--web'].some((f) => flags.has(f)) && process.stderr.isTTY) process.stderr.write('Piagent công ty: đang kết nối Agent Watch, chờ ô nhập hiện ra rồi gõ…\n');
|
|
26
28
|
// Do this before importing the SDK: it must not discover personal keys, Node
|
|
27
29
|
// preload hooks, proxy overrides or provider endpoints from an ambient shell.
|
|
28
30
|
const clean = { PATH: '/usr/bin:/bin', HOME: home, LANG: 'en_US.UTF-8', TERM: process.env.TERM || 'xterm-256color', ...interopEnvironment() };
|
|
@@ -134,7 +136,8 @@ try {
|
|
|
134
136
|
// at the same time. One the WebUI is running is refused.
|
|
135
137
|
const { holdManagedSession } = await import('../packages/piagent-webui/ownership/managed-terminal-lease.ts');
|
|
136
138
|
// The Terminal's own commands and words (updates, Bypass, Harness notes): company-terminal.mjs.
|
|
137
|
-
const { companyTerminalExtension } = await import('./company-terminal.mjs');
|
|
139
|
+
const { companyTerminalExtension, companyToolRenderers, companyInteractiveMode, harnessSummary } = await import('./company-terminal.mjs');
|
|
140
|
+
const toolRenderers = await companyToolRenderers(config.sdk_root);
|
|
138
141
|
const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
139
142
|
const terminal = companyTerminalExtension({ managed: () => current, packageRoot, sdkRoot: config.sdk_root,
|
|
140
143
|
bindingChanged: () => { try { return digest(config.entrypoint) !== config.entrypoint_sha256; } catch { return true; } } });
|
|
@@ -147,6 +150,8 @@ try {
|
|
|
147
150
|
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(); }
|
|
148
151
|
current = await ManagedSession.create({ sdkRoot: config.sdk_root, cwd, origin: config.origin, broker, agentDir, sessionManager,
|
|
149
152
|
protectedRoots: [configPath, agentDir], renewBroker: newBroker, bindingRevision, terminalExtensions: [terminal] });
|
|
153
|
+
// The plan is drawn as a checklist here; Pi keeps this definition object.
|
|
154
|
+
for (const [name, draw] of Object.entries(toolRenderers)) Object.assign(current.session.getToolDefinition(name) ?? {}, draw);
|
|
150
155
|
return { session: current.session, extensionsResult: current.extensionsResult, services: current.services, diagnostics: [] };
|
|
151
156
|
};
|
|
152
157
|
const host = await api.createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager: api.SessionManager.create(cwd, path.join(agentDir, 'sessions')) });
|
|
@@ -183,8 +188,13 @@ try {
|
|
|
183
188
|
// Pi's exit line names `pi --session-dir …`, which would open this company
|
|
184
189
|
// conversation in personal Pi; name the company Terminal instead.
|
|
185
190
|
const write = process.stdout.write.bind(process.stdout), project = /^[\w./~:-]+$/.test(cwd) ? cwd : `'${cwd.replaceAll("'", "'\\''")}'`;
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
// Pi's "Model scope: agent-watch-auto (Ctrl+P to cycle)" line names a
|
|
192
|
+
// model nobody picks; the company header says what the Harness runs.
|
|
193
|
+
process.stdout.write = (chunk, ...rest) => typeof chunk === 'string' && chunk.includes('Model scope: ') ? true
|
|
194
|
+
: 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);
|
|
195
|
+
// /share, /bug, /login, /model, /thinking, /settings and /changelog: company-terminal.mjs.
|
|
196
|
+
const CompanyMode = await companyInteractiveMode(api, { sdkRoot: config.sdk_root, packageRoot, summary: () => harnessSummary(current) });
|
|
197
|
+
await new CompanyMode(host, { verbose: false }).run();
|
|
188
198
|
}
|
|
189
199
|
}
|
|
190
200
|
} catch (error) {
|