@piagent/platform 1.48.1 → 1.48.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.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -0
- package/README.vi.md +2 -0
- package/docs/command-reference-vietnamese.md +4 -3
- package/docs/distribution-standard.md +6 -6
- package/docs/harness-conformance.md +4 -0
- 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/assets/search-en.json +1 -1
- package/docs-site/assets/search-vi.json +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/update.html +8 -0
- package/docs-site/content/update.html +8 -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 +15 -15
- 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/update.html +11 -3
- package/docs-site/en/whats-new.html +52 -27
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +15 -15
- 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/releases.json +77 -0
- 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/update.html +11 -3
- package/docs-site/whats-new.html +52 -27
- 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/extensions/piagent-guard.ts +4 -1
- package/packages/piagent-core/extensions/workspace-inspection.ts +4 -1
- package/packages/piagent-core/managed/check-waiver.mjs +35 -0
- package/packages/piagent-core/managed/command-tools.mjs +2 -2
- package/packages/piagent-core/managed/dist/workspace/manifest.json +1 -1
- package/packages/piagent-core/managed/dist/workspace/worker.mjs +7 -1
- package/packages/piagent-core/managed/patch-snapshot.mjs +28 -13
- package/packages/piagent-core/managed/review-snapshot-request.mjs +1 -1
- package/packages/piagent-core/managed/review-snapshot-worker.mjs +52 -11
- package/packages/piagent-core/managed/session.mjs +9 -9
- package/packages/piagent-core/managed/workflow.mjs +8 -7
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/update/terminal-reload.mjs +122 -0
- package/packages/piagent-core/runtime/update/terminal-update-offer.mjs +1 -1
- package/packages/piagent-webui/dist/client/assets/{ActivityPanel-DxQ3_dyT.js → ActivityPanel-Bv-jc1s3.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/App-DqN7B1JR.js +5 -0
- package/packages/piagent-webui/dist/client/assets/AttachmentImage-eZKqLYZ2.js +1 -0
- package/packages/piagent-webui/dist/client/assets/GatewayApp-B3--WjSd.js +63 -0
- package/packages/piagent-webui/dist/client/assets/{InspectionApp-pD-BqBXm.js → InspectionApp-BKQiA0Ce.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{LineReview-C_0ZpgCH.js → LineReview-CB3mQe7m.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-zw5RxDmZ.js +29 -0
- package/packages/piagent-webui/dist/client/assets/{SessionAgentWorkspace-3tM7Q6HH.js → SessionAgentWorkspace-ZAdRbxVA.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/SessionInspectorDrawer-B2e4-1FG.js +2 -0
- package/packages/piagent-webui/dist/client/assets/SettingsNavigation-C3MC0X4P.js +3 -0
- package/packages/piagent-webui/dist/client/assets/SettingsPage-D4os5vHj.js +1 -0
- package/packages/piagent-webui/dist/client/assets/{TerminalPanel-CtZyb0tj.js → TerminalPanel-CVYUiDkn.js} +2 -2
- package/packages/piagent-webui/dist/client/assets/{TerminalView-yaIg6RCs.js → TerminalView-C3upxbKw.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{WorkspaceCodeEditor-BS_ZrjPN.js → WorkspaceCodeEditor-E0Zopcke.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-DldLoJyf.js +5 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceFilePreview-CvIBYige.js +1 -0
- package/packages/piagent-webui/dist/client/assets/{WorkspaceQuickOpen-CJmMBmK8.js → WorkspaceQuickOpen-Bi-kTEEU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-DcrpSyBK.js +37 -0
- package/packages/piagent-webui/dist/client/assets/{tab-close-ITV4Cf4X.js → tab-close-BwkkhMH7.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/transcript-view-model-acWtHGyE.js +3 -0
- package/packages/piagent-webui/dist/client/assets/{ui-preferences-0waeSO-S.js → ui-preferences-OfS30-ZV.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{vendor-CSDKB7mU.js → vendor-DNZbaWhT.js} +62 -26
- package/packages/piagent-webui/dist/client/assets/{workspace-api-D8sJkiZB.js → workspace-api-CFBR73lU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/workspace-navigation-DW8zhH6U.js +1 -0
- package/packages/piagent-webui/dist/client/index.html +3 -3
- package/packages/piagent-webui/shared/process-copy.ts +2 -1
- package/schemas/piagent-webui/workspace-media-v1.schema.json +6 -0
- package/scripts/company-terminal.mjs +21 -1
- package/scripts/piagent-cli.mjs +44 -21
- package/scripts/piagent-studio.mjs +22 -2
- package/scripts/terminal-reload.mjs +15 -0
- package/scripts/update-global.mjs +10 -0
- package/packages/piagent-webui/dist/client/assets/App-fZeIRoaD.js +0 -5
- package/packages/piagent-webui/dist/client/assets/GatewayApp-CpC4lgYK.js +0 -99
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-DGt5B-6D.js +0 -29
- package/packages/piagent-webui/dist/client/assets/SessionInspectorDrawer-BJ94aQ6y.js +0 -2
- package/packages/piagent-webui/dist/client/assets/SettingsNavigation-BFfqMcuL.js +0 -3
- package/packages/piagent-webui/dist/client/assets/SettingsPage-CxpZ7cdM.js +0 -1
- package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-CkFBpcM7.js +0 -5
- package/packages/piagent-webui/dist/client/assets/index-CNdhIouM.js +0 -37
- package/packages/piagent-webui/dist/client/assets/transcript-view-model-CpEMY1xb.js +0 -3
- package/packages/piagent-webui/dist/client/assets/workspace-navigation-DkVobhwg.js +0 -1
|
@@ -27,12 +27,40 @@ function repositoryOf(file) {
|
|
|
27
27
|
for (let dir = path.posix.dirname(file); dir !== '.'; dir = path.posix.dirname(dir)) if (fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
|
+
// The repositories below a folder that is none itself (clones side by side),
|
|
31
|
+
// relative to the folder. A repository's own folders are not searched.
|
|
32
|
+
const SEARCH_DEPTH = 3, MAX_REPOSITORIES = 64, NOT_SEARCHED = new Set(['node_modules', 'vendor', 'dist', 'build', 'target']);
|
|
33
|
+
function childRepositories() {
|
|
34
|
+
const found = [];
|
|
35
|
+
const search = (dir, depth) => {
|
|
36
|
+
let entries; try { entries = fs.readdirSync(dir || '.', { withFileTypes: true }); } catch { return; }
|
|
37
|
+
for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
38
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || NOT_SEARCHED.has(entry.name) || found.length >= MAX_REPOSITORIES) continue;
|
|
39
|
+
const child = dir ? `${dir}/${entry.name}` : entry.name;
|
|
40
|
+
if (fs.existsSync(path.join(child, '.git'))) found.push(child);
|
|
41
|
+
else if (depth < SEARCH_DEPTH) search(child, depth + 1);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
search('', 1);
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
const repoGit = (repository, args) => git(['-C', repository, ...args]);
|
|
48
|
+
const repoHead = repository => { try { return repoGit(repository, ['rev-parse', '--verify', '-q', 'HEAD^{commit}']).trim(); } catch { return ''; } };
|
|
30
49
|
const mode = process.argv[2];
|
|
31
50
|
// The arguments after the mode: a scope file, then the commit the patch starts
|
|
32
51
|
// from (the turn's starting HEAD; commits made since are part of the change).
|
|
33
52
|
// While HEAD has not moved, the patch is the plain one against HEAD.
|
|
34
53
|
const head = () => { try { return git(['rev-parse', '--verify', '-q', 'HEAD^{commit}']).trim(); } catch { return ''; } };
|
|
35
54
|
if (mode === 'head') { process.stdout.write(head() || 'none'); process.exit(0); }
|
|
55
|
+
// "heads": where each repository below the folder stands, so a commit made
|
|
56
|
+
// during the turn stays part of its change, as `base` does in one repository.
|
|
57
|
+
if (mode === 'heads') {
|
|
58
|
+
const heads = insideWorkTree() ? {} : Object.fromEntries(childRepositories().map(r => [r, repoHead(r)]).filter(([, sha]) => sha));
|
|
59
|
+
const name = `heads-${randomUUID()}.json`, data = Buffer.from(JSON.stringify(heads));
|
|
60
|
+
fs.writeFileSync(path.join(os.tmpdir(), name), data, { mode: 0o600, flag: 'wx' });
|
|
61
|
+
process.stdout.write(`${sha256(data)} ${data.length} ${name}`);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
36
64
|
// "since <ref>": the commit a reference names, when HEAD descends from it (a
|
|
37
65
|
// review of the commits made since); "branch-base": where this branch left
|
|
38
66
|
// the default branch (or, on it, its upstream), when commits came since.
|
|
@@ -53,19 +81,29 @@ if (mode === 'branch-base') {
|
|
|
53
81
|
}
|
|
54
82
|
const extra = process.argv.slice(3), given = extra.find(a => /^[0-9a-f]{40}([0-9a-f]{24})?$/.test(a));
|
|
55
83
|
const base = given && given !== head() ? given : 'HEAD';
|
|
84
|
+
// `bases-….json` (from "heads"): the commit each repository below the folder
|
|
85
|
+
// started the turn on; a repository not named is compared with its HEAD.
|
|
86
|
+
const basesName = extra.find(a => a.startsWith('bases-'));
|
|
87
|
+
if (basesName !== undefined && !/^bases-[0-9a-f-]{36}\.json$/.test(basesName)) throw Error('review-scope-invalid');
|
|
88
|
+
const bases = basesName ? JSON.parse(fs.readFileSync(path.join(os.tmpdir(), basesName), 'utf8')) : {};
|
|
89
|
+
const baseOf = repository => /^[0-9a-f]{40}([0-9a-f]{24})?$/.test(bases[repository] ?? '') ? bases[repository] : 'HEAD';
|
|
56
90
|
// "files": what each changed or new file holds now (a hash, "deleted"), so
|
|
57
91
|
// the runtime can tell which files one conversation's command changed.
|
|
58
92
|
if (mode === 'files') {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
93
|
+
// In a folder of several repositories each one answers for its own files,
|
|
94
|
+
// named from the folder. Without this a command's changes there have no
|
|
95
|
+
// owner and a review of them cannot run (seen 2026-10-30: `not_git`).
|
|
96
|
+
const repositories = insideWorkTree() ? [] : childRepositories();
|
|
97
|
+
const listed = (run, from, prefix) => [...run(['diff', '--name-only', '--no-renames', '--relative', '-z', from, '--', '.']).split('\0'), ...run(['ls-files', '--others', '--exclude-standard', '-z']).split('\0')].filter(Boolean).map(name => prefix + name);
|
|
98
|
+
const all = repositories.length ? repositories.flatMap(r => { try { return listed(args => repoGit(r, args), baseOf(r), `${r}/`); } catch { return []; } }) : listed(git, base, '');
|
|
99
|
+
if (all.length > MAX_UNTRACKED * 4) throw Error('review-untracked-limit');
|
|
62
100
|
const print = name => {
|
|
63
101
|
let stat; try { stat = fs.lstatSync(name); } catch { return 'deleted'; }
|
|
64
102
|
if (stat.isSymbolicLink()) return 'link:' + sha256(fs.readlinkSync(name));
|
|
65
103
|
if (stat.isDirectory()) return 'dir:' + sha256(fs.readdirSync(name).sort().join('\0'));
|
|
66
104
|
return stat.size > HASHED_FILE ? `size:${stat.size}:${stat.mtimeMs}` : sha256(fs.readFileSync(name)) + ':' + (stat.mode & 0o777);
|
|
67
105
|
};
|
|
68
|
-
const files = Object.fromEntries([...new Set(
|
|
106
|
+
const files = Object.fromEntries([...new Set(all)].sort().map(name => [name, print(name)]));
|
|
69
107
|
const name = `files-${randomUUID()}.json`, data = Buffer.from(JSON.stringify(files));
|
|
70
108
|
fs.writeFileSync(path.join(os.tmpdir(), name), data, { mode: 0o600, flag: 'wx' });
|
|
71
109
|
process.stdout.write(`${sha256(data)} ${data.length} ${name}`);
|
|
@@ -73,20 +111,23 @@ if (mode === 'files') {
|
|
|
73
111
|
}
|
|
74
112
|
// A scope (a file the runtime wrote in this boundary's temporary directory):
|
|
75
113
|
// only these paths, the ones this conversation changed, make the patch.
|
|
76
|
-
const scopeName = extra.find(a => a !== given);
|
|
114
|
+
const scopeName = extra.find(a => a !== given && a !== basesName);
|
|
77
115
|
if (scopeName !== undefined && !/^scope-[0-9a-f-]{36}\.json$/.test(scopeName)) throw Error('review-scope-invalid');
|
|
78
116
|
const scope = scopeName ? new Set(JSON.parse(fs.readFileSync(path.join(os.tmpdir(), scopeName), 'utf8'))) : null;
|
|
79
117
|
if (scope && [...scope].some(p => typeof p !== 'string' || !p || path.isAbsolute(p) || p.split('/').includes('..'))) throw Error('review-scope-invalid');
|
|
80
118
|
// A folder that is not a repository but holds several (clones side by side):
|
|
81
119
|
// each changed path is diffed in the repository that holds it, against that
|
|
82
120
|
// repository's HEAD, with its paths kept relative to the folder. A path no
|
|
83
|
-
// repository holds is named, not read. With no scope
|
|
84
|
-
//
|
|
121
|
+
// repository holds is named, not read. With no scope, every repository's
|
|
122
|
+
// changes make the patch; a conversation with no change yet has an empty one.
|
|
85
123
|
const repositories = [], outside = [];
|
|
86
124
|
let patch = '', paths;
|
|
87
|
-
|
|
125
|
+
const several = insideWorkTree() ? [] : childRepositories();
|
|
126
|
+
if (scope && !scope.size && several.length) paths = [];
|
|
127
|
+
else if ((scope ? scope.size : several.length) && !insideWorkTree()) {
|
|
88
128
|
const groups = new Map();
|
|
89
|
-
for (const
|
|
129
|
+
if (!scope) for (const repository of several) groups.set(repository, ['.']);
|
|
130
|
+
for (const file of [...(scope ?? [])].sort()) {
|
|
90
131
|
const repository = repositoryOf(file);
|
|
91
132
|
if (repository === null) { outside.push(file); continue; }
|
|
92
133
|
if (!groups.has(repository)) groups.set(repository, []);
|
|
@@ -95,9 +136,9 @@ if (scope && scope.size && !insideWorkTree()) {
|
|
|
95
136
|
if (!groups.size) throw Error('managed-review-patch-unreadable: not a git repository');
|
|
96
137
|
paths = [];
|
|
97
138
|
for (const [repository, files] of groups) {
|
|
98
|
-
const spec = files.map(p => ':(literal)' + p), run = args =>
|
|
139
|
+
const spec = files.map(p => p === '.' ? '.' : ':(literal)' + p), run = args => repoGit(repository, args);
|
|
99
140
|
repositories.push(repository);
|
|
100
|
-
patch += run(['diff', '--binary', '--no-ext-diff', '--no-textconv', '--no-renames', `--src-prefix=a/${repository}/`, `--dst-prefix=b/${repository}/`,
|
|
141
|
+
patch += run(['diff', '--binary', '--no-ext-diff', '--no-textconv', '--no-renames', `--src-prefix=a/${repository}/`, `--dst-prefix=b/${repository}/`, baseOf(repository), '--', ...spec]);
|
|
101
142
|
paths.push(...run(['ls-files', '--others', '--exclude-standard', '-z', '--', ...spec]).split('\0').filter(Boolean).map(p => `${repository}/${p}`));
|
|
102
143
|
}
|
|
103
144
|
paths.sort();
|
|
@@ -20,7 +20,7 @@ import { runHelper } from './helper-run.mjs';
|
|
|
20
20
|
import { askTool } from './member-questions.mjs';
|
|
21
21
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
22
22
|
import { stageCompaction, compactEarly, backOffFailedCompaction, trackCompaction, boundRequestImages } from './compaction.mjs';
|
|
23
|
-
import { readPatchDigest, readPatchSnapshot, readChangedFiles, readHead } from './patch-snapshot.mjs';
|
|
23
|
+
import { readPatchDigest, readPatchSnapshot, readChangedFiles, readHead, readRepositoryHeads } from './patch-snapshot.mjs';
|
|
24
24
|
import { memberTurn } from './continuation.mjs';
|
|
25
25
|
import { workflowPolicy, repositoryChecks, sessionPlanTool, currentPlan, workflowPrompt, RunProcess, completionGate, pendingBaseline, pendingPaths, pendingHead, processEditTools } from './workflow.mjs';
|
|
26
26
|
import { workflowContextTool, workflowContextExtension } from './workflow-context.mjs';
|
|
@@ -29,13 +29,13 @@ import { HELPER_CALLS, HELPER_ROLES, helperRoles, helperPrompt, webPrompt, deleg
|
|
|
29
29
|
import { loadAgentResources } from './agent-skills.mjs';
|
|
30
30
|
import { loadPiagentThemes } from '../runtime/terminal/terminal-style.mjs';
|
|
31
31
|
import { restorePermission, recordBypass, permissionSetter } from './permission.mjs';
|
|
32
|
-
import { commandTools, servicesPrompt } from './command-tools.mjs'; import { attachMemory, memoryToolsFor, openRound, closeRound, prepareMemoryContext, trackMemoryLessons } from './memory.mjs';
|
|
32
|
+
import { commandTools, servicesPrompt } from './command-tools.mjs'; import { waiveChecksTool } from './check-waiver.mjs'; import { attachMemory, memoryToolsFor, openRound, closeRound, prepareMemoryContext, trackMemoryLessons } from './memory.mjs';
|
|
33
33
|
|
|
34
34
|
const PROVIDER = 'agent_watch_managed';
|
|
35
35
|
export const HELPER_BEAT_MS = 60_000;
|
|
36
36
|
// Who the agent is: the model account may put another product's name in an
|
|
37
37
|
// earlier system line; the member is talking to Piagent.
|
|
38
|
-
const BASE_PROMPT = 'You are Piagent, the company coding assistant. If an earlier system line gives you another product name, that line belongs to the model account: when asked who you are, say you are Piagent, the company coding assistant. Work directly on the user request. Do not require task contracts or workflow commands. ' + UNTRUSTED_RULE + ' Never claim a stale review covers changed code. When the request leaves a decision open that changes the result and you cannot settle it from the code or a subagent, ask the member with ask_user before you act on a guess. To
|
|
38
|
+
const BASE_PROMPT = 'You are Piagent, the company coding assistant. If an earlier system line gives you another product name, that line belongs to the model account: when asked who you are, say you are Piagent, the company coding assistant. Work directly on the user request. Do not require task contracts or workflow commands. ' + UNTRUSTED_RULE + ' Never claim a stale review covers changed code. When the request leaves a decision open that changes the result and you cannot settle it from the code or a subagent, ask the member with ask_user before you act on a guess. To send the member a picture or a file in the chat, keep it in the project and write  in your answer, such as  or : the web view draws a picture in place and shows any other file as a card to open or download; say the path too, for the Terminal. To point at code, link it, such as [app.ts:42](src/app.ts:42): the web view opens it at the line.';
|
|
39
39
|
// Folders outside the project the member points to (an @ mention, a folder
|
|
40
40
|
// they name) are readable for reference; the sandbox keeps them read-only.
|
|
41
41
|
const referencePrompt = home => ` The member may point you to folders outside the project for reference (an @path mention or a folder they name): read them with read, ls, grep and find. They are read-only: changes go in the project only. In those tools ~ is the member's home folder (${home}); in bash ~ is a private empty folder, so use absolute paths there.`;
|
|
@@ -129,7 +129,7 @@ export class ManagedSession {
|
|
|
129
129
|
if (roles.length) customTools.push({ name: 'delegate', label: 'Subagent', description: delegateDescription(roles) + SPEC_PROMPT,
|
|
130
130
|
parameters: delegateParameters(), // the brief and its structured fields (helper-settlement.mjs)
|
|
131
131
|
execute: (_id, args, signal) => self.delegate(delegateArgs(self, args), signal) });
|
|
132
|
-
customTools.push(sessionPlanTool(self), ...(roles.length ? [resolveTool(self)] : [])); // the objection ledger's decisions (ledger.mjs)
|
|
132
|
+
customTools.push(sessionPlanTool(self), waiveChecksTool(self), ...(roles.length ? [resolveTool(self)] : [])); // the member's waiver of checks (check-waiver.mjs), the objection ledger's decisions (ledger.mjs)
|
|
133
133
|
// The member decides what the request leaves open: numbered options, or their own words.
|
|
134
134
|
customTools.push(workflowContextTool(() => self.session.sessionManager));
|
|
135
135
|
customTools.push(askTool(self), ...memoryToolsFor(self, agentDir)); // team memory's tools, once Studio answered memory here (memory.mjs)
|
|
@@ -264,7 +264,7 @@ export class ManagedSession {
|
|
|
264
264
|
// during the turn is still its change.
|
|
265
265
|
const pending = pendingBaseline(manager);
|
|
266
266
|
self.ownPaths = new Set(pending !== null ? pendingPaths(manager) : []);
|
|
267
|
-
self.base = (pending !== null ? pendingHead(manager) : null) ?? await readHead(self.boundary);
|
|
267
|
+
self.base = (pending !== null ? pendingHead(manager) : null) ?? await readHead(self.boundary); self.repositoryBases = await readRepositoryHeads(self.boundary);
|
|
268
268
|
self.run.startDigest = pending ?? await self.digest(); await openRound(self, manager, text); // sends what waited; a new conversation gets the team brief
|
|
269
269
|
} catch (error) { self.preflight = describeFailure('main', failureText(error)); }
|
|
270
270
|
self.activePrompt = true;
|
|
@@ -430,14 +430,14 @@ export class ManagedSession {
|
|
|
430
430
|
// Whether this conversation changed code, and whether a check ran on it,
|
|
431
431
|
// look only at the files it changed: another conversation (or the member)
|
|
432
432
|
// changing other files in the same folder is not this run's change.
|
|
433
|
-
async changeDigest() { return readPatchDigest(this.boundary, this.ownPaths, this.base); }
|
|
433
|
+
async changeDigest() { return readPatchDigest(this.boundary, this.ownPaths, this.base, this.repositoryBases); }
|
|
434
434
|
async folderDigest() { try { return await readPatchDigest(this.boundary); } catch { return null; } }
|
|
435
435
|
// What a reviewer reads: this conversation's files; before it changed any,
|
|
436
436
|
// the whole folder (the member's own changes, asked to be reviewed).
|
|
437
437
|
reviewScope() { return this.ownPaths.size ? this.ownPaths : null; }
|
|
438
|
-
async patchSnapshot() { return readPatchSnapshot(this.boundary, this.reviewScope(), this.base); }
|
|
439
|
-
async patchDigest() { return readPatchDigest(this.boundary, this.reviewScope(), this.base); }
|
|
440
|
-
async changedFiles() { try { return await readChangedFiles(this.boundary, this.base); } catch { return null; } }
|
|
438
|
+
async patchSnapshot() { return readPatchSnapshot(this.boundary, this.reviewScope(), this.base, this.repositoryBases); }
|
|
439
|
+
async patchDigest() { return readPatchDigest(this.boundary, this.reviewScope(), this.base, this.repositoryBases); }
|
|
440
|
+
async changedFiles() { try { return await readChangedFiles(this.boundary, this.base, this.repositoryBases); } catch { return null; } }
|
|
441
441
|
// A write or edit names its file; a command, the files it changed.
|
|
442
442
|
claimPath(raw) {
|
|
443
443
|
if (typeof raw !== 'string' || !raw) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { reviewUnavailableReason } from './patch-snapshot.mjs';
|
|
4
|
-
import { failureCode } from '../runtime/managed-failure.mjs';
|
|
4
|
+
import { failureCode, unnamedFailureText } from '../runtime/managed-failure.mjs';
|
|
5
5
|
import { TsDiagnostics } from './ts-diagnostics.mjs';
|
|
6
6
|
import { checkpointsOf } from './checkpoints.mjs';
|
|
7
7
|
import { discoverDeliveryContext } from './delivery-context.mjs';
|
|
@@ -148,7 +148,7 @@ export function workflowPrompt(policy, checks) {
|
|
|
148
148
|
if (policy.plan !== 'off') lines.push(`- Plan: for work with several steps keep a checklist with update_plan and update it as you go. When the member asks for several items (plans, files, tickets), put every item in the checklist and work through all of them; do not end your turn to report between items.${policy.plan === 'require' ? ' For a complex task, file edits are refused until a plan exists. A checklist left with pending steps sends you back to the next one until every step is completed or deferred: if you cannot go on without the member, ask with ask_user. Mark a step deferred only when the member asked to leave it for later or it is no longer needed; never mark completed a step you did not do.' : ''}`);
|
|
149
149
|
if (policy.verify !== 'off') {
|
|
150
150
|
const list = checks.commands.length ? `${checks.commands.map(c => '`' + c + '`').join(', ')}${checks.source === 'detected' ? ' (detected; the repository declares none in AGENTS.md "## Checks")' : ''}` : 'none declared: choose the tests, type check or build that cover the change';
|
|
151
|
-
lines.push(`- Verify: after changing code, run the repository checks that cover the change and fix failures before you finish. Repository checks: ${list}.${policy.verify === 'require' ? ' The turn does not end until a check has passed on the final code.' : ''}
|
|
151
|
+
lines.push(`- Verify: after changing code, run the repository checks that cover the change and fix failures before you finish. Repository checks: ${list}.${policy.verify === 'require' ? ' The turn does not end until a check has passed on the final code.' : ''} Only the member may skip the checks: when they ask to, call waive_checks with their words; never skip them on your own.`);
|
|
152
152
|
}
|
|
153
153
|
// What each severity asks: blocking is fixed or argued, major decided, minor only listed.
|
|
154
154
|
const severities = ' Blocking findings: fix them, or reject one with resolve_objection and the reviewer judges your reason; a disagreement that remains goes to the member. Major findings: fix them or reject them with resolve_objection; no new review follows. Minor findings are listed for the member and need nothing.';
|
|
@@ -350,7 +350,7 @@ export async function completionGate(managed, run, signal) {
|
|
|
350
350
|
// The member declined the approval a check needed (a JDK, Docker, the
|
|
351
351
|
// network): asking again would only repeat the same answer.
|
|
352
352
|
if (policy.verify === 'require' && !run.verified && run.approvalDeclined && run.fixLoops > 0) verifyGaveUp = true;
|
|
353
|
-
if (policy.verify === 'require' && !run.verified && !run.docsOnly && !verifyGaveUp) {
|
|
353
|
+
if (policy.verify === 'require' && !run.verified && !run.docsOnly && !verifyGaveUp && run.waivedDigest !== digest) {
|
|
354
354
|
run.fixLoops += 1;
|
|
355
355
|
const list = run.checks.commands.length ? run.checks.commands.map(c => '`' + c + '`').join(', ') : 'the tests, type check or build that cover the change';
|
|
356
356
|
await send(`Harness process check: code changed in this turn, but no check has passed on the current code. Run the checks that cover your change (${list}) and fix what fails. If a check cannot run because the machine lacks something (a runtime such as a JDK, Docker, a database or another service, the network), request the approval that makes it run (run_with_network, run_with_docker, start_docker, run_on_host) or ask the member with ask_user, then run it; install or start nothing without that approval. If it fails for a reason unrelated to your change, say so plainly.`,
|
|
@@ -367,8 +367,9 @@ export async function completionGate(managed, run, signal) {
|
|
|
367
367
|
try { await managed.delegate({ role: 'review', task: brief }, signal, { harness: true }); }
|
|
368
368
|
catch (error) {
|
|
369
369
|
run.reviewUnavailable = !stopped(); run.reviewUnavailableReason = reviewUnavailableReason(error?.message); interrupted = stopped();
|
|
370
|
-
// Why the review could not run
|
|
371
|
-
|
|
370
|
+
// Why the review could not run: its code, and the redacted words of a
|
|
371
|
+
// failure no code names (the report says only "failed").
|
|
372
|
+
if (run.reviewUnavailable) try { session.sessionManager.appendCustomEntry('agent-watch-failure-detail', { role: 'review', code: failureCode(error?.message), ...(unnamedFailureText(error?.message) ? { detail: unnamedFailureText(error?.message) } : {}) }); } catch { /* the report still says it */ }
|
|
372
373
|
break;
|
|
373
374
|
} finally { managed.reviewFocus = null; }
|
|
374
375
|
continue;
|
|
@@ -441,8 +442,8 @@ export async function completionGate(managed, run, signal) {
|
|
|
441
442
|
if (!run.changed && !run.disputes.length && !ledgerOf(run).length) return;
|
|
442
443
|
const finalPlan = currentPlan(session.sessionManager), planOpen = run.planUpdated ? finalPlan?.plan?.filter(p => p.status === 'pending' || p.status === 'in_progress').length ?? 0 : 0;
|
|
443
444
|
const status = { verified: run.verified, reviewed: run.reviewed, blockingOpen: run.blockingOpen, checks: run.checksRun, fixLoops: run.fixLoops,
|
|
444
|
-
...(planOpen ? { planOpen } : {}), ...(run.planSkipped ? { planSkipped: true } : {}), ...(run.disputes.length ? { disputes: run.disputes.length } : {}), ...(run.changed ? {} : { unchanged: true }) };
|
|
445
|
-
const words = [...(run.changed ? [run.verified ? 'a check passed on the final code' : policy.verify === 'off' ? null : run.docsOnly ? 'only documentation changed: no check needed' : 'no check passed on the final code',
|
|
445
|
+
...(planOpen ? { planOpen } : {}), ...(run.planSkipped ? { planSkipped: true } : {}), ...(!run.verified && run.waivedDigest === digest ? { checksWaived: true } : {}), ...(run.disputes.length ? { disputes: run.disputes.length } : {}), ...(run.changed ? {} : { unchanged: true }) };
|
|
446
|
+
const words = [...(run.changed ? [run.verified ? 'a check passed on the final code' : policy.verify === 'off' ? null : run.docsOnly ? 'only documentation changed: no check needed' : run.waivedDigest === digest ? 'no check passed on the final code: the member waived the checks' : 'no check passed on the final code',
|
|
446
447
|
run.reviewed ? (run.blockingOpen ? `review: ${run.blockingOpen} blocking issue(s) still open` : 'review: no blocking issue') : policy.review === 'off' ? null : run.reviewUnavailable ? `review could not run (${run.reviewUnavailableReason})` : 'not reviewed',
|
|
447
448
|
run.planSkipped ? 'code was changed before the required plan existed' : null, planOpen ? `plan: ${planOpen} step(s) not completed` : null] : []),
|
|
448
449
|
run.disputes.length ? `${run.disputes.length} disagreement(s) between the main agent and a subagent go to the member` : null, ledgerWords(run)].filter(Boolean);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Every open Terminal moves to a new Piagent release without being closed.
|
|
2
|
+
//
|
|
3
|
+
// `piagent reload` (and `piagent-update` once it installed a release) writes
|
|
4
|
+
// one request file. Each Terminal started before that request takes it when it
|
|
5
|
+
// is idle: no run going, nothing queued, nothing typed in the editor. A run in
|
|
6
|
+
// progress is never cut: the footer says the reload waits, and it happens when
|
|
7
|
+
// the run settles. How a Terminal reloads is its own: the personal Terminal
|
|
8
|
+
// runs Pi's /reload (Piagent's extensions are read again from disk), the
|
|
9
|
+
// company Terminal starts again in place on the same conversation, since its
|
|
10
|
+
// company code is loaded once per process.
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
const REQUEST_FILE = "piagent-reload.json";
|
|
16
|
+
const POLL_MS = 2_000;
|
|
17
|
+
const STATUS_KEY = "piagent-reload";
|
|
18
|
+
|
|
19
|
+
const requestPath = (home) => path.join(home ?? os.homedir(), ".pi", REQUEST_FILE);
|
|
20
|
+
|
|
21
|
+
export function requestTerminalReload({ home, version = null, piHost = null, now = Date.now() } = {}) {
|
|
22
|
+
const file = requestPath(home);
|
|
23
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
24
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
25
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ version, piHost, at: now })}\n`, { mode: 0o600 });
|
|
26
|
+
fs.renameSync(temporary, file);
|
|
27
|
+
return { version, piHost, at: now };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readReloadRequest(home) {
|
|
31
|
+
try {
|
|
32
|
+
const value = JSON.parse(fs.readFileSync(requestPath(home), "utf8"));
|
|
33
|
+
if (!Number.isFinite(value?.at)) return null;
|
|
34
|
+
return { at: value.at, version: typeof value.version === "string" ? value.version : null, piHost: typeof value.piHost === "string" ? value.piHost : null };
|
|
35
|
+
} catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Whether this Terminal may reload now without losing anything the member did.
|
|
39
|
+
function idle(ctx) {
|
|
40
|
+
try {
|
|
41
|
+
if (typeof ctx?.isIdle === "function" && !ctx.isIdle()) return false;
|
|
42
|
+
if (typeof ctx?.hasPendingMessages === "function" && ctx.hasPendingMessages()) return false;
|
|
43
|
+
const typed = typeof ctx?.ui?.getEditorText === "function" ? ctx.ui.getEditorText() : "";
|
|
44
|
+
return !String(typed ?? "").trim();
|
|
45
|
+
} catch { return false; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// `apply(ctx, request)` reloads this Terminal; it runs at most once per
|
|
49
|
+
// process. Only Terminals with a UI take requests: scripted runs keep theirs.
|
|
50
|
+
export function watchTerminalReload(pi, { apply, home, startedAt = Date.now(), pollMs = POLL_MS }) {
|
|
51
|
+
let ctx = null, applied = false, waiting = false, timer = null;
|
|
52
|
+
// A ctx kept from an earlier session (replaced, reloaded) throws on any use:
|
|
53
|
+
// a check that meets one does nothing, and the next session_start brings a new one.
|
|
54
|
+
const check = async () => {
|
|
55
|
+
let request;
|
|
56
|
+
try {
|
|
57
|
+
if (applied || !ctx?.hasUI) return;
|
|
58
|
+
request = readReloadRequest(home);
|
|
59
|
+
if (!request || request.at <= startedAt) return;
|
|
60
|
+
if (!idle(ctx)) {
|
|
61
|
+
if (!waiting) { waiting = true; ctx.ui?.setStatus?.(STATUS_KEY, `⟳ Nạp Piagent${request.version ? ` ${request.version}` : " mới"} khi xong lượt này`); }
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
applied = true;
|
|
65
|
+
ctx.ui?.setStatus?.(STATUS_KEY, undefined);
|
|
66
|
+
} catch { return; }
|
|
67
|
+
try { await apply(ctx, request); }
|
|
68
|
+
catch (error) {
|
|
69
|
+
applied = false;
|
|
70
|
+
try { ctx.ui?.notify?.(`Chưa nạp được Piagent mới: ${error instanceof Error ? error.message : String(error)}`, "warning"); } catch { /* the session is gone */ }
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const keep = (_event, next) => { ctx = next; };
|
|
74
|
+
pi.on("session_start", (event, next) => {
|
|
75
|
+
keep(event, next);
|
|
76
|
+
if (!timer) { timer = setInterval(() => { void check(); }, pollMs); timer.unref?.(); }
|
|
77
|
+
});
|
|
78
|
+
pi.on("agent_start", keep);
|
|
79
|
+
// The moment a run settles is the moment a waiting reload can happen.
|
|
80
|
+
pi.on("agent_settled", (event, next) => { keep(event, next); setTimeout(() => { void check(); }, 300).unref?.(); });
|
|
81
|
+
pi.on("session_shutdown", () => { if (timer) clearInterval(timer); timer = null; ctx = null; });
|
|
82
|
+
return { check };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The personal Terminal: Pi's /reload reads Piagent's extensions again from
|
|
86
|
+
// disk and keeps the conversation. A request naming another Pi host than the
|
|
87
|
+
// one running cannot be met that way: the member is told to open it again.
|
|
88
|
+
const RELOADED_FROM = "PIAGENT_RELOADED_FROM";
|
|
89
|
+
// The Pi this process runs: the package of the CLI that started it.
|
|
90
|
+
function runningPiVersion() {
|
|
91
|
+
try {
|
|
92
|
+
for (let dir = path.dirname(fs.realpathSync(process.argv[1])), depth = 0; depth < 6; depth += 1, dir = path.dirname(dir)) {
|
|
93
|
+
try { const manifest = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")); if (manifest.name === "@earendil-works/pi-coding-agent") return manifest.version; } catch { /* keep walking up */ }
|
|
94
|
+
}
|
|
95
|
+
} catch { /* unknown */ }
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
export function registerPersonalTerminalReload(pi, { installed, home = undefined }) {
|
|
99
|
+
pi.registerCommand("piagent-reload", { description: "Nạp lại Piagent (bản vừa cập nhật) mà không đóng Terminal",
|
|
100
|
+
handler: async (_args, ctx) => {
|
|
101
|
+
await ctx.waitForIdle();
|
|
102
|
+
process.env[RELOADED_FROM] = installed ?? "?";
|
|
103
|
+
await ctx.reload();
|
|
104
|
+
} });
|
|
105
|
+
pi.on("session_start", (_event, ctx) => {
|
|
106
|
+
const from = process.env[RELOADED_FROM];
|
|
107
|
+
if (from === undefined) return;
|
|
108
|
+
delete process.env[RELOADED_FROM];
|
|
109
|
+
// Said once the reloaded Terminal has drawn itself: a notice during the reload is lost.
|
|
110
|
+
const text = from === installed ? `Đã nạp lại Piagent ${installed}.` : `Đã nạp Piagent ${installed} (trước đó ${from}).`;
|
|
111
|
+
setTimeout(() => { try { ctx.ui?.notify?.(text, "info"); } catch { /* the Terminal closed meanwhile */ } }, 300).unref?.();
|
|
112
|
+
});
|
|
113
|
+
watchTerminalReload(pi, { home, apply: async (ctx, request) => {
|
|
114
|
+
if (ctx.mode !== "tui") return;
|
|
115
|
+
const VERSION = runningPiVersion();
|
|
116
|
+
if (request.piHost && VERSION && request.piHost !== VERSION) {
|
|
117
|
+
ctx.ui.notify(`Piagent ${request.version ?? "mới"} chạy trên Pi ${request.piHost}, Terminal này đang dùng Pi ${VERSION}: thoát rồi mở lại \`piagent\` để dùng bản mới.`, "warning");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await pi.sendUserMessage("/piagent-reload", { expandPromptTemplates: true });
|
|
121
|
+
} });
|
|
122
|
+
}
|
|
@@ -88,7 +88,7 @@ export function registerTerminalUpdateOffer(pi, { installed, packageRoot, restar
|
|
|
88
88
|
if (!result.ok) { ctx.ui.notify(`Chưa cập nhật được Piagent. Chi tiết trong ${result.log}. Có thể chạy \`piagent-update\` trong terminal.`, "error"); return; }
|
|
89
89
|
updated = version; status(undefined);
|
|
90
90
|
const extra = afterUpdate();
|
|
91
|
-
ctx.ui.notify(`Đã cập nhật Piagent ${installed} → ${version}.
|
|
91
|
+
ctx.ui.notify(`Đã cập nhật Piagent ${installed} → ${version}. Terminal này tự nạp bản mới khi rảnh và giữ nguyên cuộc trò chuyện (hoặc thoát rồi mở lại \`${restart}\`).${extra ? ` ${extra}` : ""}`, "info");
|
|
92
92
|
};
|
|
93
93
|
const offer = async (ctx, { force = false } = {}) => {
|
|
94
94
|
if (asking || updated || !ctx?.hasUI || ctx.ui?.[GATEWAY_UI] || !ctx.ui?.select) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e,r as t}from"./rolldown-runtime-hePW80VL.js";import{
|
|
1
|
+
import{n as e,r as t}from"./rolldown-runtime-hePW80VL.js";import{An as n,Rn as r}from"./vendor-DNZbaWhT.js";import{i,n as a}from"./ui-preferences-OfS30-ZV.js";import{B as o,x as s}from"./api-BQS4Dtrr.js";import{c}from"./view-model-CWNiS4JN.js";var l=t(r(),1);function u(e,t=`vi`){let n=e.state===`running`?`running`:e.state===`failed`?`failed`:`passed`,r=n===`running`?t===`vi`?`đang chạy`:`running`:n===`failed`?t===`vi`?`thất bại`:`failed`:t===`vi`?`hoàn tất`:`completed`,i=e.startedAt??e.finishedAt??``;return{activityRef:`live.${e.toolCallRef}`,kind:[`bash`,`shell`,`exec`].includes(e.toolLabel)?`command`:`tool`,state:n,label:`${e.toolLabel} ${r}`,preview:t===`vi`?`Cập nhật trực tiếp từ Gateway`:`Live update from the Gateway`,toolCallId:e.toolCallRef,toolName:e.toolLabel,commandDigest:null,logRef:null,exitCode:null,exitCodeExact:!1,startedAt:i,finishedAt:n===`running`?null:e.finishedAt??i}}function d(e,t=`vi`){let n=e.state===`failed`?t===`vi`?`Lượt chạy gặp lỗi`:`Operation failed`:e.state===`blocked`?t===`vi`?`Lượt chạy bị chặn`:`Operation blocked`:e.state===`aborted`?t===`vi`?`Lượt chạy đã dừng`:`Operation stopped`:t===`vi`?`Chưa xác định kết quả lượt chạy`:`Operation outcome unknown`;return{activityRef:e.activityRef,kind:`system`,state:e.state,label:n,preview:e.reasonCode,toolCallId:null,toolName:null,commandDigest:null,logRef:null,exitCode:null,exitCodeExact:!1,startedAt:e.settledAt,finishedAt:e.settledAt}}function f(e,t,n,r=`vi`,i){let a=[...e,...t],o=new Set(a.map(e=>e.activityRef)),s=n.filter(e=>!o.has(e.activityRef)).map(e=>d(e,r));if(i===void 0)return{rows:[...e,...s,...t],terminalCount:s.length,runningCount:e.length};let c=i.filter(e=>e.state===`running`).map(e=>u(e,r));return{rows:[...c,...s,...t],terminalCount:s.length,runningCount:c.length}}function p(e,t=`vi`){return e.state===`passed`&&/ recovered$/i.test(e.label)?t===`vi`?`Đã khôi phục`:`Recovered`:e.state===`passed`&&/ warning$/i.test(e.label)?t===`vi`?`Đã xử lý cảnh báo`:`Handled warning`:e.state===`passed`&&e.exitCodeExact?`Pass · exit ${e.exitCode??0}`:e.state===`failed`&&e.exitCodeExact?`Fail · exit ${e.exitCode??`?`}`:e.state===`blocked`?`Blocked`:e.state===`aborted`?t===`vi`?`Đã dừng`:`Stopped`:e.state===`unknown`?t===`vi`?`Chưa xác định`:`Unknown`:e.state===`failed`?t===`vi`?`Thất bại`:`Failed`:e.state===`running`?t===`vi`?`Đang chạy`:`Running`:e.state.replaceAll(`-`,` `)}function m(e,t=`vi`){let n=Date.parse(e.startedAt),r=e.finishedAt?Date.parse(e.finishedAt):Date.now();if(!Number.isFinite(n)||!Number.isFinite(r)||r<n)return t===`vi`?`Thời gian chưa xác định`:`Unknown duration`;let i=Math.round((r-n)/1e3);return i<60?`${i}s`:`${Math.floor(i/60)}m ${i%60}s`}var h=e({ActivityPanel:()=>v}),g=n();function _({activity:e,preview:t,onPreview:n,locale:r}){return(0,g.jsxs)(`article`,{className:`activity-row`,children:[(0,g.jsx)(`span`,{className:`activity-state tone-${c(e.state)}`,"aria-label":e.state}),(0,g.jsxs)(`div`,{className:`activity-copy`,children:[(0,g.jsxs)(`div`,{children:[(0,g.jsx)(`strong`,{children:e.label}),(0,g.jsx)(`span`,{children:e.kind})]}),(0,g.jsx)(`p`,{children:e.preview||a(r,`Không có mô tả ngắn`,`No short description`)}),t&&(0,g.jsxs)(`div`,{className:`log-preview ${t.state===`error`?`error-state`:``}`,children:[t.state===`loading`?a(r,`Đang tải log ngắn…`,`Loading log preview…`):t.state===`error`?a(r,`Log preview không còn sẵn sàng.`,`Log preview is no longer available.`):t.value?.preview??`${a(r,`Không có nội dung log`,`No log content`)} · ${t.value?.reasonCode??`unavailable`}`,t.value?.truncated&&(0,g.jsx)(`small`,{children:a(r,`Preview đã được rút gọn`,`Preview truncated`)})]})]}),(0,g.jsxs)(`div`,{className:`activity-result`,children:[(0,g.jsx)(`strong`,{children:p(e,r)}),(0,g.jsx)(`span`,{children:m(e,r)}),e.logRef&&n&&(0,g.jsx)(`button`,{type:`button`,onClick:n,"aria-expanded":!!t,children:t?a(r,`Ẩn log`,`Hide log`):a(r,`Xem log`,`View log`)})]})]})}function v({snapshot:e,sessionRef:t,terminalActivities:n=[],liveActivities:r}){let{locale:c}=i(),[u,d]=(0,l.useState)({}),p=e?.activity.running??[],m=e?.activity.recent??[],{rows:h,terminalCount:v,runningCount:y}=f(p,m,n,c,r),b=e=>{if(u[e.activityRef]){d(t=>({...t,[e.activityRef]:void 0}));return}d(t=>({...t,[e.activityRef]:{state:`loading`}})),(t?o(t,e.activityRef):s(e.activityRef)).then(t=>d(n=>({...n,[e.activityRef]:{state:`ready`,value:t}}))).catch(()=>d(t=>({...t,[e.activityRef]:{state:`error`}})))};return(0,g.jsxs)(`section`,{className:`activity-panel surface`,"aria-labelledby":`activity-title`,children:[(0,g.jsxs)(`header`,{className:`panel-heading`,children:[(0,g.jsxs)(`div`,{children:[(0,g.jsx)(`p`,{className:`section-kicker`,children:`Activity`}),(0,g.jsx)(`h2`,{id:`activity-title`,children:a(c,`Tool, command và kết quả lượt chạy`,`Tools, commands, and operation outcomes`)})]}),(0,g.jsxs)(`div`,{children:[(0,g.jsxs)(`span`,{className:`running-count`,children:[y,` `,a(c,`đang chạy`,`running`)]}),(0,g.jsxs)(`span`,{children:[m.length+v,` `,a(c,`gần đây`,`recent`)]})]})]}),(0,g.jsxs)(`div`,{className:`activity-list`,children:[h.map(e=>(0,g.jsx)(_,{activity:e,preview:u[e.activityRef],onPreview:e.logRef?()=>b(e):void 0,locale:c},e.activityRef)),h.length===0&&(0,g.jsx)(`div`,{className:`activity-empty`,children:a(c,`Chưa có tool, command hoặc kết quả lượt chạy nào trong timeline hiện tại.`,`No tools, commands, or operation outcomes in the current timeline.`)})]}),e?.activity.page.truncated&&(0,g.jsx)(`footer`,{className:`panel-footnote`,children:a(c,`Timeline đã được rút gọn theo giới hạn local.`,`The timeline was truncated to local limits.`)})]})}export{h as n,v as t};
|