agentgui 1.0.1126 → 1.0.1127
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.
Potentially problematic release.
This version of agentgui might be problematic. Click here for more details.
- package/.gm/browser-config.json +1 -0
- package/.gm/disciplines/agentgui/mem-1779277005143-0-1352.json +1 -0
- package/.gm/disciplines/agentgui/memories/.flat-export-done +0 -0
- package/.gm/disciplines/agentgui/memories/mem-c07fa3b0836edbe2-1352.md +8 -0
- package/.gm/gm.db +0 -0
- package/.gm/memories/.flat-export-done +1 -0
- package/.gm/memories/mem-0131ff4869d86085-370.md +10 -0
- package/.gm/memories/mem-04049d11cb8c1661-2135.md +8 -0
- package/.gm/memories/mem-069ff10372ed1770-1375.md +8 -0
- package/.gm/memories/mem-08a4a9e050a4deff-3048.md +8 -0
- package/.gm/memories/mem-09597ee0df9c9224-965.md +8 -0
- package/.gm/memories/mem-0993d156f571b619-1059.md +8 -0
- package/.gm/memories/mem-0b681bf2340c8df1-3170.md +8 -0
- package/.gm/memories/mem-0c444abde86202f6-1681.md +8 -0
- package/.gm/memories/mem-0d1408fa048b9923-1159.md +8 -0
- package/.gm/memories/mem-0dad5d82fec829ba-887.md +8 -0
- package/.gm/memories/mem-10a776effb7ce3a6-935.md +8 -0
- package/.gm/memories/mem-10d264b4f8e66fb4-1492.md +8 -0
- package/.gm/memories/mem-118c35ac67541d98-2909.md +8 -0
- package/.gm/memories/mem-119cf70dd19aa0a5-2225.md +8 -0
- package/.gm/memories/mem-15a07cd761b337c4-4320.md +8 -0
- package/.gm/memories/mem-15c73aca528535cb-2183.md +8 -0
- package/.gm/memories/mem-166f52796185e0e7-1487.md +8 -0
- package/.gm/memories/mem-1a148cf9c3126df5-3963.md +8 -0
- package/.gm/memories/mem-1a459001058f618e-567.md +8 -0
- package/.gm/memories/mem-1af3e13f7618e668-2146.md +8 -0
- package/.gm/memories/mem-1fd31d6873e9c164-794.md +8 -0
- package/.gm/memories/mem-21f551be092fd77d-2340.md +8 -0
- package/.gm/memories/mem-231ff5de003f20f0-586.md +8 -0
- package/.gm/memories/mem-2567393d3adb03ab-347.md +8 -0
- package/.gm/memories/mem-27faf41cdd050c80-1261.md +8 -0
- package/.gm/memories/mem-28889e43e8ce64cc-3663.md +8 -0
- package/.gm/memories/mem-2f4fd06515abf439-2562.md +8 -0
- package/.gm/memories/mem-34a749430303c5f7-544.md +8 -0
- package/.gm/memories/mem-3602d11d7bce9446-1057.md +8 -0
- package/.gm/memories/mem-38b7e24dbcba2f3e-639.md +8 -0
- package/.gm/memories/mem-39a7cc2f887a90b1-1079.md +8 -0
- package/.gm/memories/mem-40d6169c8759061d-806.md +8 -0
- package/.gm/memories/mem-4420e6d00e5c8632-2318.md +8 -0
- package/.gm/memories/mem-447e5a54ddc9357a-654.md +8 -0
- package/.gm/memories/mem-455b3beb4afabe14-591.md +8 -0
- package/.gm/memories/mem-4845ee594759151c-560.md +8 -0
- package/.gm/memories/mem-4a1254c60b868b88-374.md +8 -0
- package/.gm/memories/mem-4bafafa47a0e4b6c-3237.md +8 -0
- package/.gm/memories/mem-4c06c45b4d31bc60-1202.md +8 -0
- package/.gm/memories/mem-4dfac801d0378448-3676.md +8 -0
- package/.gm/memories/mem-4ecd3b713e384170-122.md +10 -0
- package/.gm/memories/mem-51d8992a3c6f6c9c-1073.md +8 -0
- package/.gm/memories/mem-539eee7e8c6c10ee-2557.md +8 -0
- package/.gm/memories/mem-559b8b2abc4b1091-1665.md +8 -0
- package/.gm/memories/mem-59dd73a463e54948-871.md +8 -0
- package/.gm/memories/mem-5b99b882ffe1a3c2-2262.md +8 -0
- package/.gm/memories/mem-5feb252a5244149c-1791.md +8 -0
- package/.gm/memories/mem-6224f8d185797f2b-1047.md +8 -0
- package/.gm/memories/mem-6434ee8261a6a6a2-3126.md +8 -0
- package/.gm/memories/mem-66b0947b946f3437-1155.md +8 -0
- package/.gm/memories/mem-6bdf783a1f6be7c2-1062.md +8 -0
- package/.gm/memories/mem-6d49f08bebac54c9-744.md +8 -0
- package/.gm/memories/mem-70d96acb736af2d7-685.md +8 -0
- package/.gm/memories/mem-70de7d5e71fcd439-2680.md +8 -0
- package/.gm/memories/mem-713614f12d7029da-1577.md +8 -0
- package/.gm/memories/mem-720d76c88e64f6ee-1967.md +8 -0
- package/.gm/memories/mem-7535f34f430fc196-1340.md +8 -0
- package/.gm/memories/mem-861a588d2773681a-1311.md +8 -0
- package/.gm/memories/mem-8af7980a92e09959-891.md +8 -0
- package/.gm/memories/mem-8b438ef4c609b7af-401.md +8 -0
- package/.gm/memories/mem-8f59884039787ef3-743.md +8 -0
- package/.gm/memories/mem-92b199c2024ee1aa-2513.md +8 -0
- package/.gm/memories/mem-9329105b05b9c0f6-324.md +10 -0
- package/.gm/memories/mem-94f30cec3c283f97-782.md +10 -0
- package/.gm/memories/mem-956d4813319f65a3-1710.md +8 -0
- package/.gm/memories/mem-96fffbb6615e031b-1263.md +8 -0
- package/.gm/memories/mem-993ab31f7a10aefa-718.md +8 -0
- package/.gm/memories/mem-a3e440db285f655a-995.md +8 -0
- package/.gm/memories/mem-a52933cd6495e6f5-2624.md +8 -0
- package/.gm/memories/mem-a86426ac44138c96-1606.md +8 -0
- package/.gm/memories/mem-a95b1d49d71133ce-1007.md +8 -0
- package/.gm/memories/mem-a9968a186368b022-1150.md +8 -0
- package/.gm/memories/mem-aaeae38d8eb01809-1076.md +8 -0
- package/.gm/memories/mem-ade01d9c02677c55-1557.md +8 -0
- package/.gm/memories/mem-b06b8c85b81663e6-1250.md +8 -0
- package/.gm/memories/mem-b451e5e17e555202-1490.md +8 -0
- package/.gm/memories/mem-b64ad00dcb6f909e-937.md +8 -0
- package/.gm/memories/mem-b7d6018ceca55b6f-2220.md +8 -0
- package/.gm/memories/mem-bdf466854032cac9-1509.md +8 -0
- package/.gm/memories/mem-c0482039e6ab4c50-2047.md +8 -0
- package/.gm/memories/mem-c4aaec5c4e6f80c2-920.md +8 -0
- package/.gm/memories/mem-c66976ddacb4fbe8-1454.md +8 -0
- package/.gm/memories/mem-c7c5bc263850c332-1844.md +8 -0
- package/.gm/memories/mem-cda639547932c796-713.md +8 -0
- package/.gm/memories/mem-cdf04ff6a6a0aa6f-671.md +8 -0
- package/.gm/memories/mem-cf8472c84d9e04c4-1883.md +8 -0
- package/.gm/memories/mem-cfd4d894370c63c3-1288.md +8 -0
- package/.gm/memories/mem-d060480435f92eec-1146.md +8 -0
- package/.gm/memories/mem-d347c921b79e6d44-1099.md +8 -0
- package/.gm/memories/mem-d3c8ea6c861bfbb8-1457.md +8 -0
- package/.gm/memories/mem-d5e928c552cd87f9-214.md +10 -0
- package/.gm/memories/mem-d63bc3b8a0269844-406.md +10 -0
- package/.gm/memories/mem-d9c19257abc2fa5e-1061.md +8 -0
- package/.gm/memories/mem-d9f26358a7cd3bb8-866.md +8 -0
- package/.gm/memories/mem-da1c91acf63d81d3-863.md +8 -0
- package/.gm/memories/mem-dc1ac3b04d22317e-664.md +8 -0
- package/.gm/memories/mem-dceeae20bc56d407-711.md +8 -0
- package/.gm/memories/mem-dd98b21c0896dc04-1557.md +8 -0
- package/.gm/memories/mem-df74674eec7745ac-178.md +10 -0
- package/.gm/memories/mem-dfe3b466fd5d1199-802.md +8 -0
- package/.gm/memories/mem-e0f9286aed2d5a06-1116.md +8 -0
- package/.gm/memories/mem-e1950b064099b7be-1895.md +8 -0
- package/.gm/memories/mem-e1d5431ae2542872-961.md +8 -0
- package/.gm/memories/mem-e490a181de6469fc-487.md +8 -0
- package/.gm/memories/mem-e72c6efbb27326fb-392.md +8 -0
- package/.gm/memories/mem-e943b58115a02b4c-593.md +8 -0
- package/.gm/memories/mem-e9bc5f9ed6edbcf7-1580.md +8 -0
- package/.gm/memories/mem-eae1e383927854fb-2649.md +8 -0
- package/.gm/memories/mem-ebbe6c0694c526f0-1392.md +8 -0
- package/.gm/memories/mem-ebf85becc23410b0-1633.md +8 -0
- package/.gm/memories/mem-ee0596fa17c7102e-362.md +10 -0
- package/.gm/memories/mem-ee4450fbc311b583-1361.md +8 -0
- package/.gm/memories/mem-ee8858162518f144-1252.md +8 -0
- package/.gm/memories/mem-f3297e82df622649-1166.md +8 -0
- package/.gm/memories/mem-f3f317c083d979ee-1210.md +8 -0
- package/.gm/memories/mem-f714a86465fba158-2041.md +8 -0
- package/.gm/memories/mem-f786bc1626d92845-204.md +10 -0
- package/.gm/memories/mem-f9f0dc8895b21e18-1217.md +8 -0
- package/.gm/memories/mem-fb3b2a7395800041-525.md +8 -0
- package/.gm/memories/mem-fc5753f92ee7d654-966.md +8 -0
- package/.gm/memories/mem-fe3faa1652f07239-480.md +8 -0
- package/.gm/memories/mem-ff8669e527e5c427-2788.md +8 -0
- package/.gm/mutables.yml +53 -0
- package/.gm/prd.yml +3342 -1
- package/AGENTS.md +122 -122
- package/UX_OPTIMIZATION_SUMMARY.md +238 -0
- package/agentgui-after.png +0 -0
- package/agentgui-current.png +0 -0
- package/agentgui-final.png +0 -0
- package/agentgui-nobrand.png +0 -0
- package/agentgui-now.png +0 -0
- package/agentgui-v2.png +0 -0
- package/agentgui-v4.png +0 -0
- package/bash.exe.stackdump +28 -0
- package/database.js +110 -109
- package/design-reference.png +0 -0
- package/lib/acp-sdk-manager.js +9 -42
- package/lib/claude-runner-acp.js +155 -213
- package/lib/claude-runner-agents.js +157 -170
- package/lib/http-handler.js +931 -400
- package/lib/ws-handlers-util.js +466 -11
- package/package.json +1 -1
- package/site/app/js/app.js +4741 -4221
- package/site/app/js/backend.js +602 -621
- package/site/app/vendor/anentrypoint-design/247420.css +13673 -14799
- package/site/app/vendor/anentrypoint-design/247420.js +449 -735
- package/.gm/.embed-generation.code_chunks +0 -1
- package/.gm/.embed-generation.git_commit_vectors +0 -1
- package/.gm/.embed-generation.memories +0 -1
- package/.gm/.embed-generation.rssearch_vectors +0 -1
- package/lib/acp-http-protocol.js +0 -95
- package/lib/http-routes/mutations.js +0 -199
- package/lib/http-routes/reads.js +0 -212
- package/lib/http-routes/shared.js +0 -225
- package/lib/ws-handlers/agents.js +0 -137
- package/lib/ws-handlers/chat.js +0 -157
- package/lib/ws-handlers/git.js +0 -159
- package/lib/ws-handlers/misc.js +0 -36
- package/lib/ws-handlers/shared.js +0 -47
- package/lib/ws-handlers/terminal-state.js +0 -16
- package/site/app/js/chat-persistence.js +0 -123
- package/site/app/js/hash-routing.js +0 -62
- package/site/app/js/history.js +0 -435
- package/site/app/js/shortcuts.js +0 -128
package/lib/ws-handlers/git.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { execSync, execFileSync } from 'child_process';
|
|
5
|
-
import { err, execFileP, resolveGitCwd, assertSafeRelPath, assertSafeBranch } from './shared.js';
|
|
6
|
-
|
|
7
|
-
export function register(router, deps) {
|
|
8
|
-
const { STARTUP_CWD } = deps;
|
|
9
|
-
|
|
10
|
-
router.handle('clone', (p) => {
|
|
11
|
-
const repo = (p.repo || '').trim();
|
|
12
|
-
if (!repo || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repo)) {
|
|
13
|
-
err(400, 'Invalid repo format. Use org/repo or user/repo');
|
|
14
|
-
}
|
|
15
|
-
const cloneDir = STARTUP_CWD || os.homedir();
|
|
16
|
-
const repoName = repo.split('/')[1];
|
|
17
|
-
const targetPath = path.join(cloneDir, repoName);
|
|
18
|
-
if (fs.existsSync(targetPath)) err(409, `Directory already exists: ${repoName}`);
|
|
19
|
-
try {
|
|
20
|
-
execFileSync('git', ['clone', 'https://github.com/' + repo + '.git'], {
|
|
21
|
-
cwd: cloneDir, encoding: 'utf-8', timeout: 120000,
|
|
22
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
23
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
24
|
-
});
|
|
25
|
-
return { ok: true, repo, path: targetPath, name: repoName };
|
|
26
|
-
} catch (e) { err(500, (e.stderr || e.message || 'Clone failed').trim()); }
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
router.handle('git.check', () => {
|
|
30
|
-
try {
|
|
31
|
-
const isWindows = os.platform() === 'win32';
|
|
32
|
-
const devnull = isWindows ? '' : ' 2>/dev/null';
|
|
33
|
-
const remoteUrl = execSync('git remote get-url origin' + devnull, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows }).trim();
|
|
34
|
-
const statusResult = execSync('git status --porcelain' + devnull, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
|
|
35
|
-
const hasChanges = statusResult.trim().length > 0;
|
|
36
|
-
const unpushedResult = execSync('git rev-list --count --not --remotes' + devnull, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
|
|
37
|
-
const hasUnpushed = parseInt(unpushedResult.trim() || '0', 10) > 0;
|
|
38
|
-
const githubUser = process.env.GITHUB_USER;
|
|
39
|
-
const ownsRemote = !remoteUrl.includes('github.com/') || (!!githubUser && remoteUrl.includes(githubUser));
|
|
40
|
-
return { ownsRemote, hasChanges, hasUnpushed, remoteUrl };
|
|
41
|
-
} catch {
|
|
42
|
-
return { ownsRemote: false, hasChanges: false, hasUnpushed: false, remoteUrl: '' };
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
router.handle('git.push', (p) => {
|
|
47
|
-
if (!p?.confirm) err(400, 'confirm required: git.push commits and pushes the entire working tree');
|
|
48
|
-
try {
|
|
49
|
-
const isWindows = os.platform() === 'win32';
|
|
50
|
-
const cmd = isWindows
|
|
51
|
-
? 'git add -A & git commit -m "Auto-commit" & git push'
|
|
52
|
-
: 'git add -A && git commit -m "Auto-commit" && git push';
|
|
53
|
-
execSync(cmd, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
|
|
54
|
-
return { success: true };
|
|
55
|
-
} catch (e) { err(500, e.message); }
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// --- git.status: changed-file list (staged/unstaged/untracked) for
|
|
59
|
-
// GitStatusPanel. Porcelain=v1 line format "XY PATH" (rename: "XY OLD -> NEW"),
|
|
60
|
-
// X = index state, Y = worktree state, '?'/' ' = untouched/untracked.
|
|
61
|
-
router.handle('git.status', async (p) => {
|
|
62
|
-
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
63
|
-
try {
|
|
64
|
-
const { stdout } = await execFileP('git', ['status', '--porcelain=v1'], { cwd });
|
|
65
|
-
const files = [];
|
|
66
|
-
for (const line of stdout.split('\n')) {
|
|
67
|
-
if (!line) continue;
|
|
68
|
-
const x = line[0], y = line[1];
|
|
69
|
-
const filePath = line.slice(3);
|
|
70
|
-
if (x === '?' && y === '?') { files.push({ path: filePath, status: '?' }); continue; }
|
|
71
|
-
if (x !== ' ' && x !== '?') files.push({ path: filePath, status: x, staged: true });
|
|
72
|
-
else if (y !== ' ' && y !== '?') files.push({ path: filePath, status: y, staged: false });
|
|
73
|
-
}
|
|
74
|
-
return { files };
|
|
75
|
-
} catch (e) { err(500, (e.stderr || e.message || 'git status failed').trim()); }
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// --- git.diff: unified diff for a single file or the whole working tree.
|
|
79
|
-
// p.cwd (optional) is confined via confineToRoots/fsAllowRoots, same as
|
|
80
|
-
// chat.sendMessage; p.file (optional) is validated to reject flags/shell
|
|
81
|
-
// metacharacters. execFile array-args form only, never a shell string.
|
|
82
|
-
router.handle('git.diff', async (p) => {
|
|
83
|
-
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
84
|
-
const args = ['diff', '--no-color'];
|
|
85
|
-
if (p?.staged) args.push('--staged');
|
|
86
|
-
if (p?.file) args.push('--', assertSafeRelPath(p.file, 'file'));
|
|
87
|
-
try {
|
|
88
|
-
const { stdout } = await execFileP('git', args, { cwd });
|
|
89
|
-
// git prints "Binary files a/x and b/x differ" for binary paths instead
|
|
90
|
-
// of a unified diff - an empty `diff` string then reads to the client as
|
|
91
|
-
// "no diff to show" with no explanation. Detect it so the UI can say why.
|
|
92
|
-
const binary = /^Binary files .* differ$/m.test(stdout);
|
|
93
|
-
return { diff: stdout, binary, file: p?.file || null, staged: !!p?.staged };
|
|
94
|
-
} catch (e) { err(500, (e.stderr || e.message || 'git diff failed').trim()); }
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// --- git.log: recent commit list, limit param (default 20, capped 200).
|
|
98
|
-
router.handle('git.log', async (p) => {
|
|
99
|
-
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
100
|
-
const limit = Math.max(1, Math.min(200, parseInt(p?.limit, 10) || 20));
|
|
101
|
-
const FIELD_SEP = '\x1f';
|
|
102
|
-
const REC_SEP = '\x1e';
|
|
103
|
-
const args = ['log', `-n${limit}`, `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ad${FIELD_SEP}%s${REC_SEP}`, '--date=iso-strict'];
|
|
104
|
-
try {
|
|
105
|
-
const { stdout } = await execFileP('git', args, { cwd });
|
|
106
|
-
const commits = stdout.split(REC_SEP).map(s => s.trim()).filter(Boolean).map(rec => {
|
|
107
|
-
const [hash, author, date, ...rest] = rec.split(FIELD_SEP);
|
|
108
|
-
return { hash, author, date, subject: rest.join(FIELD_SEP) };
|
|
109
|
-
});
|
|
110
|
-
return { commits };
|
|
111
|
-
} catch (e) { err(500, (e.stderr || e.message || 'git log failed').trim()); }
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// --- worktree.list / create / remove ---
|
|
115
|
-
router.handle('worktree.list', async (p) => {
|
|
116
|
-
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
117
|
-
try {
|
|
118
|
-
const { stdout } = await execFileP('git', ['worktree', 'list', '--porcelain'], { cwd });
|
|
119
|
-
// Parse the porcelain block format: records separated by blank lines,
|
|
120
|
-
// each line "key value" or a bare flag ("bare", "detached", "locked").
|
|
121
|
-
const worktrees = [];
|
|
122
|
-
let cur = null;
|
|
123
|
-
for (const line of stdout.split('\n')) {
|
|
124
|
-
if (!line.trim()) { if (cur) { worktrees.push(cur); cur = null; } continue; }
|
|
125
|
-
if (!cur) cur = {};
|
|
126
|
-
const sp = line.indexOf(' ');
|
|
127
|
-
if (sp === -1) { cur[line] = true; continue; }
|
|
128
|
-
cur[line.slice(0, sp)] = line.slice(sp + 1);
|
|
129
|
-
}
|
|
130
|
-
if (cur) worktrees.push(cur);
|
|
131
|
-
return { worktrees: worktrees.map(w => ({ path: w.worktree || null, head: w.HEAD || null, branch: w.branch || null, bare: !!w.bare, detached: !!w.detached, locked: !!w.locked })) };
|
|
132
|
-
} catch (e) { err(500, (e.stderr || e.message || 'git worktree list failed').trim()); }
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
router.handle('worktree.create', async (p) => {
|
|
136
|
-
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
137
|
-
const wtPath = assertSafeRelPath(p?.path, 'path');
|
|
138
|
-
const args = ['worktree', 'add'];
|
|
139
|
-
if (p?.newBranch) { args.push('-b', assertSafeBranch(p.newBranch, 'newBranch')); }
|
|
140
|
-
args.push(wtPath);
|
|
141
|
-
if (p?.branch) args.push(assertSafeBranch(p.branch, 'branch'));
|
|
142
|
-
try {
|
|
143
|
-
const { stdout, stderr } = await execFileP('git', args, { cwd });
|
|
144
|
-
return { ok: true, output: (stdout || stderr || '').trim(), path: wtPath };
|
|
145
|
-
} catch (e) { err(500, (e.stderr || e.message || 'git worktree add failed').trim()); }
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
router.handle('worktree.remove', async (p) => {
|
|
149
|
-
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
150
|
-
const wtPath = assertSafeRelPath(p?.path, 'path');
|
|
151
|
-
const args = ['worktree', 'remove'];
|
|
152
|
-
if (p?.force) args.push('--force');
|
|
153
|
-
args.push(wtPath);
|
|
154
|
-
try {
|
|
155
|
-
const { stdout, stderr } = await execFileP('git', args, { cwd });
|
|
156
|
-
return { ok: true, output: (stdout || stderr || '').trim() };
|
|
157
|
-
} catch (e) { err(500, (e.stderr || e.message || 'git worktree remove failed').trim()); }
|
|
158
|
-
});
|
|
159
|
-
}
|
package/lib/ws-handlers/misc.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { err } from './shared.js';
|
|
5
|
-
|
|
6
|
-
export function register(router, deps) {
|
|
7
|
-
const { queries, wsOptimizer, getProviderConfigs, saveProviderConfig, STARTUP_CWD } = deps;
|
|
8
|
-
|
|
9
|
-
router.handle('home', () => ({ home: os.homedir(), cwd: STARTUP_CWD }));
|
|
10
|
-
|
|
11
|
-
router.handle('folders', (p) => {
|
|
12
|
-
const folderPath = p.path || STARTUP_CWD;
|
|
13
|
-
try {
|
|
14
|
-
const raw = folderPath.startsWith('~') ? folderPath.replace('~', os.homedir()) : folderPath;
|
|
15
|
-
const entries = fs.readdirSync(path.resolve(raw), { withFileTypes: true });
|
|
16
|
-
return { folders: entries.filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => ({ name: e.name })).sort((a, b) => a.name.localeCompare(b.name)) };
|
|
17
|
-
} catch (e) { err(400, e.message); }
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
router.handle('auth.configs', () => getProviderConfigs());
|
|
21
|
-
|
|
22
|
-
router.handle('auth.save', (p) => {
|
|
23
|
-
const { providerId, apiKey, defaultModel } = p;
|
|
24
|
-
if (typeof providerId !== 'string' || !providerId.length || providerId.length > 100) err(400, 'Invalid providerId');
|
|
25
|
-
if (typeof apiKey !== 'string' || !apiKey.length || apiKey.length > 10000) err(400, 'Invalid apiKey');
|
|
26
|
-
if (defaultModel !== undefined && (typeof defaultModel !== 'string' || defaultModel.length > 200)) err(400, 'Invalid defaultModel');
|
|
27
|
-
const configPath = saveProviderConfig(providerId, apiKey, defaultModel || '');
|
|
28
|
-
return { success: true, path: configPath };
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
router.handle('import.claude', () => ({ imported: queries.importClaudeCodeConversations() }));
|
|
32
|
-
|
|
33
|
-
router.handle('discover.claude', () => ({ discovered: queries.discoverClaudeCodeConversations() }));
|
|
34
|
-
|
|
35
|
-
router.handle('ws.stats', () => wsOptimizer.getStats());
|
|
36
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'child_process';
|
|
2
|
-
import { confineToRoots, fsAllowRoots } from '../http-handler.js';
|
|
3
|
-
|
|
4
|
-
export function err(code, message) { const e = new Error(message); e.code = code; throw e; }
|
|
5
|
-
|
|
6
|
-
// Promisified execFile — array-args form only, never a shell string, so a
|
|
7
|
-
// crafted branch/path/file value cannot be interpreted as a shell command.
|
|
8
|
-
export function execFileP(cmd, args, opts) {
|
|
9
|
-
return new Promise((resolve, reject) => {
|
|
10
|
-
execFile(cmd, args, { encoding: 'utf-8', timeout: 30000, maxBuffer: 20 * 1024 * 1024, ...opts }, (e, stdout, stderr) => {
|
|
11
|
-
if (e) { e.stdout = stdout; e.stderr = stderr; reject(e); return; }
|
|
12
|
-
resolve({ stdout, stderr });
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Resolve + confine a client-supplied cwd to the same allowlist the file
|
|
18
|
-
// explorer / chat.sendMessage use (fsAllowRoots via confineToRoots). Falls
|
|
19
|
-
// back to STARTUP_CWD (itself an allowed root) when no cwd is given.
|
|
20
|
-
export function resolveGitCwd(p, STARTUP_CWD) {
|
|
21
|
-
if (!p?.cwd) return STARTUP_CWD;
|
|
22
|
-
const conf = confineToRoots(p.cwd, fsAllowRoots());
|
|
23
|
-
if (!conf.ok) err(conf.reason === 'not found' ? 400 : 403, `cwd outside allowed roots: ${p.cwd}`);
|
|
24
|
-
return conf.realPath;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// A relative in-repo file path: no leading '-' (flag injection), no shell
|
|
28
|
-
// metacharacters, no absolute path (git resolves it relative to cwd already).
|
|
29
|
-
const SAFE_REL_PATH_RE = /^[^\0]{1,4096}$/;
|
|
30
|
-
export function assertSafeRelPath(v, label) {
|
|
31
|
-
if (typeof v !== 'string' || !v.length) err(400, `${label} required`);
|
|
32
|
-
if (v.startsWith('-')) err(400, `${label} must not start with '-'`);
|
|
33
|
-
if (/[\0]/.test(v)) err(400, `${label} contains invalid characters`);
|
|
34
|
-
if (!SAFE_REL_PATH_RE.test(v)) err(400, `${label} invalid`);
|
|
35
|
-
return v;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// A git branch/ref name — conservative allowlist (git's own ref-format is
|
|
39
|
-
// more permissive, but this blocks every shell-metacharacter and flag-
|
|
40
|
-
// injection vector while still covering normal branch names).
|
|
41
|
-
const SAFE_BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9_.\/-]{0,199}$/;
|
|
42
|
-
export function assertSafeBranch(v, label) {
|
|
43
|
-
if (typeof v !== 'string' || !v.length) err(400, `${label} required`);
|
|
44
|
-
if (v.startsWith('-') || v.includes('..') || v.includes('//')) err(400, `${label} invalid`);
|
|
45
|
-
if (!SAFE_BRANCH_RE.test(v)) err(400, `${label} invalid`);
|
|
46
|
-
return v;
|
|
47
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// Short-lived per-session terminal-event buffer (finding 35): a turn that
|
|
2
|
-
// completes/errors/cancels while a client ws is down would otherwise be a
|
|
3
|
-
// fire-and-forget broadcast the client never sees, hanging it busy forever.
|
|
4
|
-
// A re-subscribing client gets the buffered terminal frame replayed.
|
|
5
|
-
const TERMINAL_TTL_MS = 60000;
|
|
6
|
-
const terminalEvents = new Map(); // sessionId -> terminal event
|
|
7
|
-
|
|
8
|
-
export function recordTerminal(sessionId, event) {
|
|
9
|
-
terminalEvents.set(sessionId, event);
|
|
10
|
-
const t = setTimeout(() => {
|
|
11
|
-
if (terminalEvents.get(sessionId) === event) terminalEvents.delete(sessionId);
|
|
12
|
-
}, TERMINAL_TTL_MS);
|
|
13
|
-
if (typeof t.unref === 'function') t.unref();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function getTerminal(sessionId) { return terminalEvents.get(sessionId); }
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
export const CHAT_KEY = 'agentgui.chat';
|
|
2
|
-
// Cap the persisted footprint: the last 100 messages, with any tool args/result
|
|
3
|
-
// payload over 4KB truncated in the SAVED copy only (in-memory state keeps the
|
|
4
|
-
// full payload). A quota failure is surfaced once instead of silently dropping
|
|
5
|
-
// persistence forever.
|
|
6
|
-
const PERSIST_MSG_CAP = 100;
|
|
7
|
-
const PERSIST_PART_CAP = 4096;
|
|
8
|
-
|
|
9
|
-
// An assistant turn with no content AND no parts is an empty shell (an aborted
|
|
10
|
-
// turn that produced nothing). The view already renders it as nothing; drop it
|
|
11
|
-
// from persisted/in-memory state so a restored chat never carries blank bubbles.
|
|
12
|
-
function isEmptyTurn(m) {
|
|
13
|
-
return m.role === 'assistant' && !m.content && !(Array.isArray(m.parts) && m.parts.length);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function trimPartForStorage(p) {
|
|
17
|
-
if (!p || typeof p !== 'object') return p;
|
|
18
|
-
let out = p;
|
|
19
|
-
for (const k of ['result', 'args']) {
|
|
20
|
-
const v = out[k];
|
|
21
|
-
if (v == null) continue;
|
|
22
|
-
let s;
|
|
23
|
-
if (typeof v === 'string') s = v;
|
|
24
|
-
else { try { s = JSON.stringify(v); } catch { s = String(v); } }
|
|
25
|
-
if (s.length > PERSIST_PART_CAP) {
|
|
26
|
-
if (out === p) out = { ...p };
|
|
27
|
-
out[k] = s.slice(0, PERSIST_PART_CAP);
|
|
28
|
-
out.truncatedForStorage = true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function computeTotalCost(state) {
|
|
35
|
-
return (state.chat.messages || []).reduce((s, m) => s + (typeof m.costUsd === 'number' ? m.costUsd : 0), 0);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Factory: app.js calls this once at module load, passing the live `state`
|
|
39
|
-
// object (mutated by reference, same as every other app.js concern) and the
|
|
40
|
-
// shared helpers (announce/scheduleRender/lsGet/lsSet/lsRemove/debounce)
|
|
41
|
-
// already defined there. Returns the same function set app.js previously
|
|
42
|
-
// defined inline, wired to app.js's own state/render/announce.
|
|
43
|
-
export function createChatPersistence(state, { announce, scheduleRender, lsGet, lsRemove, debounce }) {
|
|
44
|
-
let _persistFailedOnce = false;
|
|
45
|
-
let _lastPersistTs = 0;
|
|
46
|
-
|
|
47
|
-
function persistChat() {
|
|
48
|
-
try {
|
|
49
|
-
const eligible = state.chat.messages.filter(m => !isEmptyTurn(m));
|
|
50
|
-
// On-screen transcript can exceed the cap; a reload only restores the last
|
|
51
|
-
// PERSIST_MSG_CAP turns - surface that once so it's not a silent surprise,
|
|
52
|
-
// same Alert pattern as the sibling quota-failure banner below.
|
|
53
|
-
const wasCapped = eligible.length > PERSIST_MSG_CAP;
|
|
54
|
-
if (wasCapped !== !!state.chat.persistTruncated) {
|
|
55
|
-
state.chat.persistTruncated = wasCapped;
|
|
56
|
-
scheduleRender();
|
|
57
|
-
}
|
|
58
|
-
const msgs = eligible
|
|
59
|
-
.slice(-PERSIST_MSG_CAP)
|
|
60
|
-
.map(m => ({ id: m.id, role: m.role, content: m.content, time: m.time, costUsd: m.costUsd, stopped: m.stopped || undefined, parts: Array.isArray(m.parts) ? m.parts.map(trimPartForStorage) : m.parts }));
|
|
61
|
-
const draft = (state.chat.draft || '').trim() ? state.chat.draft : '';
|
|
62
|
-
// Persist when there is a transcript OR a non-empty draft (a typed-but-not-
|
|
63
|
-
// sent message must survive a reload too).
|
|
64
|
-
if (!msgs.length && !draft) { lsRemove(CHAT_KEY); return; }
|
|
65
|
-
_lastPersistTs = Date.now();
|
|
66
|
-
localStorage.setItem(CHAT_KEY, JSON.stringify({ ts: _lastPersistTs, messages: msgs, draft, resumeSid: state.chat.resumeSid, totalCost: state.chat.totalCost || 0, agent: state.selectedAgent, model: state.selectedModel }));
|
|
67
|
-
} catch {
|
|
68
|
-
if (!_persistFailedOnce) {
|
|
69
|
-
_persistFailedOnce = true;
|
|
70
|
-
state.chat.persistError = true;
|
|
71
|
-
announce('chat too large to save locally - export it from settings');
|
|
72
|
-
scheduleRender();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const debouncedPersistDraft = debounce(persistChat, 500);
|
|
78
|
-
|
|
79
|
-
// Another GUI tab rewrote the shared chat key: never silently diverge - surface
|
|
80
|
-
// a banner offering to reload the newer copy (last-writer-wins with a ts guard).
|
|
81
|
-
window.addEventListener('storage', (e) => {
|
|
82
|
-
if (e.key !== CHAT_KEY || !e.newValue) return;
|
|
83
|
-
try {
|
|
84
|
-
const remote = JSON.parse(e.newValue);
|
|
85
|
-
if (remote && remote.ts && remote.ts > _lastPersistTs) {
|
|
86
|
-
state.chat.externalUpdate = true;
|
|
87
|
-
scheduleRender();
|
|
88
|
-
}
|
|
89
|
-
} catch {}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
function restoreChat() {
|
|
93
|
-
try {
|
|
94
|
-
const raw = lsGet(CHAT_KEY);
|
|
95
|
-
if (!raw) return;
|
|
96
|
-
const saved = JSON.parse(raw);
|
|
97
|
-
if (typeof saved.draft === 'string' && saved.draft.trim()) {
|
|
98
|
-
state.chat.draft = saved.draft;
|
|
99
|
-
// A restored draft is set programmatically, so Ctrl+Z has nothing in the
|
|
100
|
-
// browser's native undo history to act on - a one-time note explains why
|
|
101
|
-
// undo appears to do nothing right after a reload.
|
|
102
|
-
announce('draft restored - note: undo history does not carry over a reload');
|
|
103
|
-
}
|
|
104
|
-
if (typeof saved.totalCost === 'number') state.chat.totalCost = saved.totalCost;
|
|
105
|
-
if (Array.isArray(saved.messages) && saved.messages.length) {
|
|
106
|
-
state.chat.messages = saved.messages
|
|
107
|
-
.filter(m => !isEmptyTurn(m))
|
|
108
|
-
.map(m => ({ ...m, parts: Array.isArray(m.parts) ? m.parts : [] }));
|
|
109
|
-
// Prefer the per-message cost sum when present (it self-corrects after
|
|
110
|
-
// edit/retry truncation); the scalar is only the legacy fallback.
|
|
111
|
-
const derived = computeTotalCost(state);
|
|
112
|
-
if (derived) state.chat.totalCost = derived;
|
|
113
|
-
state.chat.resumeSid = saved.resumeSid || null;
|
|
114
|
-
// Restore the agent/model the transcript belongs to, so a restored chat
|
|
115
|
-
// isn't silently shown under whatever agent the picker defaulted to.
|
|
116
|
-
state.chat.restoredAgent = saved.agent || null;
|
|
117
|
-
state.chat.restoredModel = saved.model || null;
|
|
118
|
-
}
|
|
119
|
-
} catch {}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return { persistChat, restoreChat, debouncedPersistDraft, isEmptyTurn, computeTotalCost: () => computeTotalCost(state) };
|
|
123
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// Hash-based deep-link state, extracted from app.js (vertical slice per
|
|
2
|
-
// AGENTS.md SOLID/Clean-Architecture preferences). Full hash routing state is
|
|
3
|
-
// HASH_KEYS; popstate diffs the full set, and navTo(tab,{writeHash:false})
|
|
4
|
-
// navigates without pushing a new hash entry (navTo itself stays in app.js -
|
|
5
|
-
// it orchestrates tab-switch side effects like reloading history/files/live
|
|
6
|
-
// data, not just the hash string, so it is a broader concern than routing).
|
|
7
|
-
export const HASH_KEYS = ['tab', 'sid', 'dir', 'file', 'q', 'project', 'section', 'filter', 'lsort', 'lfilter', 'lerr', 'ets'];
|
|
8
|
-
|
|
9
|
-
export function readHash() {
|
|
10
|
-
const hash = location.hash || '';
|
|
11
|
-
const out = {};
|
|
12
|
-
for (const k of HASH_KEYS) {
|
|
13
|
-
const m = hash.match(new RegExp('(?:^#|&)' + k + '=([^&]*)'));
|
|
14
|
-
out[k] = m ? decodeURIComponent(m[1]) : null;
|
|
15
|
-
}
|
|
16
|
-
return out;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function buildHash(state) {
|
|
20
|
-
const parts = [];
|
|
21
|
-
const tab = state.tab || 'chat';
|
|
22
|
-
// tab is omitted for the default chat tab, EXCEPT when a session id rides
|
|
23
|
-
// along - a bare #sid= historically meant history, so chat+sid must name its
|
|
24
|
-
// tab explicitly or the deep-link restores the wrong surface.
|
|
25
|
-
if (tab !== 'chat') parts.push('tab=' + encodeURIComponent(tab));
|
|
26
|
-
else if (state.selectedSid) parts.push('tab=chat');
|
|
27
|
-
// Keep sid whenever set (regardless of tab) so Back restores the selection.
|
|
28
|
-
if (state.selectedSid) parts.push('sid=' + encodeURIComponent(state.selectedSid));
|
|
29
|
-
if (tab === 'files' && state.files) {
|
|
30
|
-
if (state.files.path) parts.push('dir=' + encodeURIComponent(state.files.path));
|
|
31
|
-
if (state.files.preview && state.files.preview.path) parts.push('file=' + encodeURIComponent(state.files.preview.path));
|
|
32
|
-
if (state.files.filter) parts.push('filter=' + encodeURIComponent(state.files.filter));
|
|
33
|
-
}
|
|
34
|
-
if (tab === 'history') {
|
|
35
|
-
const q = (state.searchQ || '').trim();
|
|
36
|
-
if (q.length >= 2) parts.push('q=' + encodeURIComponent(q));
|
|
37
|
-
if (state.projectFilter) parts.push('project=' + encodeURIComponent(state.projectFilter));
|
|
38
|
-
// A session opened from a search hit carries the matched event's
|
|
39
|
-
// timestamp so reload/Back reproduces the same scrolled+flashed position
|
|
40
|
-
// a live click gives - without this, the anchor only existed in memory
|
|
41
|
-
// and a search-hit URL degraded to just the bare session on reload.
|
|
42
|
-
if (state._focusEventTs != null) parts.push('ets=' + encodeURIComponent(state._focusEventTs));
|
|
43
|
-
}
|
|
44
|
-
if (tab === 'settings' && state.settingsSection) parts.push('section=' + encodeURIComponent(state.settingsSection));
|
|
45
|
-
if (tab === 'live') {
|
|
46
|
-
const lv = state.live || {};
|
|
47
|
-
if (lv.sort && lv.sort !== 'status') parts.push('lsort=' + encodeURIComponent(lv.sort));
|
|
48
|
-
if (lv.filter) parts.push('lfilter=' + encodeURIComponent(lv.filter));
|
|
49
|
-
if (lv.errorsOnly) parts.push('lerr=1');
|
|
50
|
-
}
|
|
51
|
-
return parts.length ? '#' + parts.join('&') : '';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function writeHash(state, { push = false } = {}) {
|
|
55
|
-
const h = buildHash(state);
|
|
56
|
-
const url = location.pathname + location.search + h;
|
|
57
|
-
if (location.hash === h || (!location.hash && !h)) return;
|
|
58
|
-
// pushState for user navigation steps (tab visits, session selection,
|
|
59
|
-
// directory walks, preview opens) so Back retraces them; replaceState for
|
|
60
|
-
// passive state sync (search text, filter resets).
|
|
61
|
-
(push ? history.pushState : history.replaceState).call(history, null, '', url);
|
|
62
|
-
}
|