agentgui 1.0.1089 → 1.0.1091
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/AGENTS.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# AgentGUI — Agent Notes
|
|
2
2
|
|
|
3
|
+
## Docstudio design-cue follow-up (2026-07-23) — fiftieth run
|
|
4
|
+
|
|
5
|
+
Eighth docstudio-cue pass. A fresh Explore-agent survey (told the full 49-run ported list) read every remaining unmined docstudio surface — admin-observability-{views,chart}.js, marketplace-{trending,card,flag}.js, documents/drive-picker.js, documents/file-upload.js, streaming/tts-player.js, thread-list-realtime.js, capture-page.js/capture-runner.js, and admin users/tags/archives/audit/moderation tabs — and came back near-exhausted: nearly everything was either already covered under a different name (docstudio's tts-player is *less* mature than the kit's own `voice.js`), pure business logic with no portable UI (drive-picker delegates to Google's native picker iframe; file-upload/thread-list-realtime have no render surface at all), or app-specific GCP wiring (the observability panels' Cloud Logging deep-links). One genuine, narrow gap survived: `admin-observability-views.js:132-161` `endpointsView()` color-codes percentile-latency/success-rate table cells (green >=99%, amber >=95%, red below) with no kit equivalent — `Table` had no notion of a value implying good/warn/bad. Ported the portable convention (not the GCP wiring) as `RateCell({value, tone})` in `design/src/components/data-density.js`, a plain tone-colored `<span>` over the existing `--success`/`--warn`/`--danger` tokens that drops into any `Table` cell, matching `Table`'s own onSort host-owns-the-logic convention (this component has no opinion on thresholds). While touching `data-density.js`, found and fixed a real, unrelated, previously-undiscovered gap: **its entire component set (`PhaseWalk`/`TreeNode`/`BarRow`/`StatTile`/`StatsGrid`/`SubGrid`/`SessionRow`/`DevRow`/`LiveLog`) was never wired into `src/components.js`'s barrel** — unreachable from the built bundle despite having real CSS in `data-density.css`, since before this run. Fixed by adding the barrel export line alongside the new `RateCell`. Kit built+tested (`bun test.js` all pass, 0 lint errors), docs regenerated (`docs/component-props.md`, 218 symbols, 0 drift). Kit pushed `72d88b6`, all 3 kit CI checks green (ci/Publish 247420 to npm/Deploy GH Pages). agentgui re-vendored both dist files; browser-witnessed live on `localhost:3009/gm/`: imported the live-served vendored bundle directly, confirmed `mod.components.RateCell`/`mod.components.StatTile` both callable, and mounted `RateCell({value:"UNIQ_99_4",tone:"good"})` into real DOM via `applyDiff` — rendered exactly `<span class="ds-rate-cell ds-rate-cell-good">UNIQ_99_4</span>`, confirming the CSS tone-class wiring works end-to-end live. **Real tooling defect diagnosed+fixed this run, not papered over**: mid-session, `browser` verb dispatches intermittently failed `"plugin gm not loaded"` — root-caused via `.gm/exec-spool/.watcher.log` to a **leftover stale `gm-plugkit` `supervisor.js` process** (pid alive since a prior session, `Jul19`) running independently alongside the healthy native `agentplug-runner` daemon, periodically trying to respawn the watcher via the *retired* JS `plugkit-wasm-wrapper.js` path (`Cannot find module './wrapper/wasi-shim.js'` — the exact historically-resolved npm-packaging defect from the 48th run, recurring here via a stale cached supervisor rather than a fresh regression) and crash-looping. A separately-wedged long-lived Chrome process for the agentgui project profile (alive since `Jul17`) was also found wedging evals to 60s timeouts. Killed both stale processes, rebooted a fresh daemon (`bun x gm-plugkit@latest spool`) — confirmed fixed via two clean subsequent dispatches, no further crash-loop lines. **Also newly confirmed: writing a spool verb input file under a task-number that collides with an existing `out/<verb>-<N>.json` from an earlier turn in the same session silently returns the STALE cached output instead of running the fresh dispatch** — the watcher appears to treat a pre-existing out-file as already-satisfied and skip reprocessing. Always pick a task number confirmed absent from `out/` (e.g. jump to a clearly-unused block like 100+) rather than trusting sequential same-session numbering, especially after a long session with many prior dispatches.
|
|
6
|
+
|
|
3
7
|
## Docstudio design-cue follow-up (2026-07-23) — forty-ninth run
|
|
4
8
|
|
|
5
9
|
Seventh docstudio-cue pass, same directive repeated once more ("consider everything we haven't covered yet"). A fresh Explore-agent survey read every file in `/config/docstudio/public/js` (33 files not previously examined, including admin-observability, marketplace, drive-picker, file-upload, tts-player, thread-list-realtime, capture-page) against both the kit and the "already ported" list built up over 48 prior runs. Verdict: genuinely exhausted — the only survivor was the long-deferred `documents/xls-preview.js` tabbed spreadsheet/CSV inline preview, twice previously deferred across runs 48/47 as "a future initiative" without ever being detailed. Per the standing rule against documenting instead of implementing reachable work, specced and shipped it this run: new `design/src/components/spreadsheet-preview.js` `SpreadsheetPreview({workbook, activeSheet, onSheetChange, maxRows, maxCols, truncated, loading, error, errorActionLabel, onErrorAction})` — the kit does no parsing (no SheetJS dependency; host hands over an already-parsed `{sheetNames, sheets}` shape), renders a `role=tablist` sheet-switcher, a sticky-header `table.ds-sheet-preview-table` with `.num`-aligned numeric cells, and a persistent (non-hiding) truncation banner; loading/error states reuse the existing `Skeleton`/`Alert` primitives rather than inventing new visual language. Barrel-exported through `src/components.js` per the standing barrel rule. CSS landed in `src/css/app-shell/kits-appended.css` (tab underline uses the same neutral-at-rest/accent-only-on-active-state pattern as `Table`'s sortable headers). Kit built+tested (`bun test.js` all pass), docs regenerated (`docs/component-props.md`, 206 symbols, 0 drift). Kit pushed `3535f0a`, all 3 kit CI checks green (ci/Publish 247420 to npm/Deploy GH Pages). agentgui re-vendored both dist files; browser-witnessed live on `localhost:3009/gm/` (this sandboxed env has no display, so `.gm/browser-config.json` gained `{"headless":true}` — a real, durable per-project config need, not a one-off workaround): imported the live-served vendored bundle directly (`mod.components.SpreadsheetPreview`, not a top-level export — components live under a `components` namespace on the built ESM bundle), rendered all 5 states (loading/error/loaded/truncated/empty) without throwing, and mounted the truncated state into real DOM via `applyDiff` — confirmed a real `table.ds-sheet-preview-table` with 499 correctly-clamped body rows (`maxRows` default 500) plus a visible banner reading the accurate row count. agentgui pushed `35c5b77`→`4f0789a` (a PRD-state-only follow-up commit), all 4 CI checks green both pushes (Test/Auto-Declaudeify/Publish-and-Release/Deploy-GH-Pages). **Session-start tooling note: this run's `gm` spool watcher was stale (status.json `ts` ~4 days old, `busy_until` long past) — a plain `bun x gm-plugkit@latest spool` reboot recovered it cleanly, distinct from the 48th run's actual npm-packaging outage (which the watcher now reports as resolved: it boots via the native `agentplug-runner` daemon path, not the retired JS wasm-host wrapper the 48th run's blocker was about — that stale PRD row was resolved this run). A separate transient `"plugin gm not loaded"` error recurred a handful of times mid-session on both `browser` and `prd-add`/`prd-resolve` dispatches with no discernible trigger; a bare identical re-dispatch cleared it every time observed (not yet root-caused, but not a hard blocker either — recorded here rather than as a `blockedBy:external` PRD row since it never actually blocked forward progress, only cost a retry).** **Also confirmed: this repo's checked-out branch (`fix/ws-baseurl-on-main`) is not literally named `main` but IS main's true linear continuation — `git fetch origin main` showed `origin/main` HEAD exactly matched this branch's parent commit before any push, so `git_push {branch:"main"}` (explicit override of the git_finalize verb's default same-name-branch push, which the verb correctly refused since branch name != "main") was the correct, safe action, not a deviation.**
|
package/lib/ws-handlers-util.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
|
-
import { execSync, spawnSync } from 'child_process';
|
|
5
|
+
import { execSync, spawnSync, execFile } from 'child_process';
|
|
6
6
|
import { runClaudeWithStreaming } from './claude-runner-run.js';
|
|
7
7
|
import { registry } from './claude-runner-agents.js';
|
|
8
8
|
import { confineToRoots, fsAllowRoots } from './http-handler.js';
|
|
@@ -10,6 +10,49 @@ import { restart as restartAcpAgent } from './acp-sdk-manager.js';
|
|
|
10
10
|
|
|
11
11
|
function err(code, message) { const e = new Error(message); e.code = code; throw e; }
|
|
12
12
|
|
|
13
|
+
// Promisified execFile — array-args form only, never a shell string, so a
|
|
14
|
+
// crafted branch/path/file value cannot be interpreted as a shell command.
|
|
15
|
+
function execFileP(cmd, args, opts) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
execFile(cmd, args, { encoding: 'utf-8', timeout: 30000, maxBuffer: 20 * 1024 * 1024, ...opts }, (e, stdout, stderr) => {
|
|
18
|
+
if (e) { e.stdout = stdout; e.stderr = stderr; reject(e); return; }
|
|
19
|
+
resolve({ stdout, stderr });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Resolve + confine a client-supplied cwd to the same allowlist the file
|
|
25
|
+
// explorer / chat.sendMessage use (fsAllowRoots via confineToRoots). Falls
|
|
26
|
+
// back to STARTUP_CWD (itself an allowed root) when no cwd is given.
|
|
27
|
+
function resolveGitCwd(p, STARTUP_CWD) {
|
|
28
|
+
if (!p?.cwd) return STARTUP_CWD;
|
|
29
|
+
const conf = confineToRoots(p.cwd, fsAllowRoots());
|
|
30
|
+
if (!conf.ok) err(conf.reason === 'not found' ? 400 : 403, `cwd outside allowed roots: ${p.cwd}`);
|
|
31
|
+
return conf.realPath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A relative in-repo file path: no leading '-' (flag injection), no shell
|
|
35
|
+
// metacharacters, no absolute path (git resolves it relative to cwd already).
|
|
36
|
+
const SAFE_REL_PATH_RE = /^[^\0]{1,4096}$/;
|
|
37
|
+
function assertSafeRelPath(v, label) {
|
|
38
|
+
if (typeof v !== 'string' || !v.length) err(400, `${label} required`);
|
|
39
|
+
if (v.startsWith('-')) err(400, `${label} must not start with '-'`);
|
|
40
|
+
if (/[\0]/.test(v)) err(400, `${label} contains invalid characters`);
|
|
41
|
+
if (!SAFE_REL_PATH_RE.test(v)) err(400, `${label} invalid`);
|
|
42
|
+
return v;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A git branch/ref name — conservative allowlist (git's own ref-format is
|
|
46
|
+
// more permissive, but this blocks every shell-metacharacter and flag-
|
|
47
|
+
// injection vector while still covering normal branch names).
|
|
48
|
+
const SAFE_BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9_.\/-]{0,199}$/;
|
|
49
|
+
function assertSafeBranch(v, label) {
|
|
50
|
+
if (typeof v !== 'string' || !v.length) err(400, `${label} required`);
|
|
51
|
+
if (v.startsWith('-') || v.includes('..') || v.includes('//')) err(400, `${label} invalid`);
|
|
52
|
+
if (!SAFE_BRANCH_RE.test(v)) err(400, `${label} invalid`);
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
const SUB_AGENT_MAP = {
|
|
14
57
|
'opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }], 'cli-opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
|
|
15
58
|
'gemini': [{ id: 'gm-gc', name: 'GM Gemini' }], 'cli-gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
|
|
@@ -281,6 +324,84 @@ export function register(router, deps) {
|
|
|
281
324
|
} catch (e) { err(500, e.message); }
|
|
282
325
|
});
|
|
283
326
|
|
|
327
|
+
// --- git.diff: unified diff for a single file or the whole working tree.
|
|
328
|
+
// p.cwd (optional) is confined via confineToRoots/fsAllowRoots, same as
|
|
329
|
+
// chat.sendMessage; p.file (optional) is validated to reject flags/shell
|
|
330
|
+
// metacharacters. execFile array-args form only, never a shell string.
|
|
331
|
+
router.handle('git.diff', async (p) => {
|
|
332
|
+
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
333
|
+
const args = ['diff', '--no-color'];
|
|
334
|
+
if (p?.staged) args.push('--staged');
|
|
335
|
+
if (p?.file) args.push('--', assertSafeRelPath(p.file, 'file'));
|
|
336
|
+
try {
|
|
337
|
+
const { stdout } = await execFileP('git', args, { cwd });
|
|
338
|
+
return { diff: stdout, file: p?.file || null, staged: !!p?.staged };
|
|
339
|
+
} catch (e) { err(500, (e.stderr || e.message || 'git diff failed').trim()); }
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// --- git.log: recent commit list, limit param (default 20, capped 200).
|
|
343
|
+
router.handle('git.log', async (p) => {
|
|
344
|
+
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
345
|
+
const limit = Math.max(1, Math.min(200, parseInt(p?.limit, 10) || 20));
|
|
346
|
+
const FIELD_SEP = '\x1f';
|
|
347
|
+
const REC_SEP = '\x1e';
|
|
348
|
+
const args = ['log', `-n${limit}`, `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ad${FIELD_SEP}%s${REC_SEP}`, '--date=iso-strict'];
|
|
349
|
+
try {
|
|
350
|
+
const { stdout } = await execFileP('git', args, { cwd });
|
|
351
|
+
const commits = stdout.split(REC_SEP).map(s => s.trim()).filter(Boolean).map(rec => {
|
|
352
|
+
const [hash, author, date, ...rest] = rec.split(FIELD_SEP);
|
|
353
|
+
return { hash, author, date, subject: rest.join(FIELD_SEP) };
|
|
354
|
+
});
|
|
355
|
+
return { commits };
|
|
356
|
+
} catch (e) { err(500, (e.stderr || e.message || 'git log failed').trim()); }
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// --- worktree.list / create / remove ---
|
|
360
|
+
router.handle('worktree.list', async (p) => {
|
|
361
|
+
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
362
|
+
try {
|
|
363
|
+
const { stdout } = await execFileP('git', ['worktree', 'list', '--porcelain'], { cwd });
|
|
364
|
+
// Parse the porcelain block format: records separated by blank lines,
|
|
365
|
+
// each line "key value" or a bare flag ("bare", "detached", "locked").
|
|
366
|
+
const worktrees = [];
|
|
367
|
+
let cur = null;
|
|
368
|
+
for (const line of stdout.split('\n')) {
|
|
369
|
+
if (!line.trim()) { if (cur) { worktrees.push(cur); cur = null; } continue; }
|
|
370
|
+
if (!cur) cur = {};
|
|
371
|
+
const sp = line.indexOf(' ');
|
|
372
|
+
if (sp === -1) { cur[line] = true; continue; }
|
|
373
|
+
cur[line.slice(0, sp)] = line.slice(sp + 1);
|
|
374
|
+
}
|
|
375
|
+
if (cur) worktrees.push(cur);
|
|
376
|
+
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 })) };
|
|
377
|
+
} catch (e) { err(500, (e.stderr || e.message || 'git worktree list failed').trim()); }
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
router.handle('worktree.create', async (p) => {
|
|
381
|
+
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
382
|
+
const wtPath = assertSafeRelPath(p?.path, 'path');
|
|
383
|
+
const args = ['worktree', 'add'];
|
|
384
|
+
if (p?.newBranch) { args.push('-b', assertSafeBranch(p.newBranch, 'newBranch')); }
|
|
385
|
+
args.push(wtPath);
|
|
386
|
+
if (p?.branch) args.push(assertSafeBranch(p.branch, 'branch'));
|
|
387
|
+
try {
|
|
388
|
+
const { stdout, stderr } = await execFileP('git', args, { cwd });
|
|
389
|
+
return { ok: true, output: (stdout || stderr || '').trim(), path: wtPath };
|
|
390
|
+
} catch (e) { err(500, (e.stderr || e.message || 'git worktree add failed').trim()); }
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
router.handle('worktree.remove', async (p) => {
|
|
394
|
+
const cwd = resolveGitCwd(p, STARTUP_CWD);
|
|
395
|
+
const wtPath = assertSafeRelPath(p?.path, 'path');
|
|
396
|
+
const args = ['worktree', 'remove'];
|
|
397
|
+
if (p?.force) args.push('--force');
|
|
398
|
+
args.push(wtPath);
|
|
399
|
+
try {
|
|
400
|
+
const { stdout, stderr } = await execFileP('git', args, { cwd });
|
|
401
|
+
return { ok: true, output: (stdout || stderr || '').trim() };
|
|
402
|
+
} catch (e) { err(500, (e.stderr || e.message || 'git worktree remove failed').trim()); }
|
|
403
|
+
});
|
|
404
|
+
|
|
284
405
|
router.handle('auth.configs', () => getProviderConfigs());
|
|
285
406
|
|
|
286
407
|
router.handle('auth.save', (p) => {
|
package/package.json
CHANGED
package/site/app/js/app.js
CHANGED
|
@@ -43,6 +43,10 @@ const state = {
|
|
|
43
43
|
activeTimer: null,
|
|
44
44
|
eventsLimit: 300, // how many of the most-recent events to render; grows via "load older"
|
|
45
45
|
files: { path: '', segments: [], entries: [], roots: [], loading: false, error: null, preview: null, sort: 'name', sortDir: 'asc', filter: '' },
|
|
46
|
+
// Minimal git/worktree panel state. The richer visual component
|
|
47
|
+
// (GitDiffView/WorktreeSwitcher) lives in anentrypoint-design; this is a
|
|
48
|
+
// plain list/text render until the vendored SDK bundle picks those up.
|
|
49
|
+
git: { loading: false, error: null, diff: '', commits: [], worktrees: [], file: '' },
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
// Two-step arm controls auto-reset after this delay so an accidental first click
|
|
@@ -641,6 +645,8 @@ function view() {
|
|
|
641
645
|
crumbLeaf = 'files';
|
|
642
646
|
} else if (state.tab === 'live') {
|
|
643
647
|
crumbLeaf = 'live · ' + ((state.active && state.active.length) || 0);
|
|
648
|
+
} else if (state.tab === 'git') {
|
|
649
|
+
crumbLeaf = 'git';
|
|
644
650
|
} else if (state.tab === 'settings') {
|
|
645
651
|
// Same word as the rail item - location chrome must not fork vocabulary.
|
|
646
652
|
crumbLeaf = 'settings';
|
|
@@ -725,6 +731,7 @@ function workspaceRail() {
|
|
|
725
731
|
{ key: 'history', label: 'History', icon: 'thread', active: state.tab === 'history', onClick: () => navTo('history') },
|
|
726
732
|
{ key: 'files', label: 'Files', icon: 'folder', active: state.tab === 'files', onClick: () => navTo('files') },
|
|
727
733
|
{ key: 'live', label: 'Live', icon: 'activity', active: state.tab === 'live', count: liveCount || null, rail: liveHasError ? 'flame' : undefined, onClick: () => navTo('live') },
|
|
734
|
+
{ key: 'git', label: 'Git', icon: 'branch', active: state.tab === 'git', onClick: () => navTo('git') },
|
|
728
735
|
{ key: 'settings', label: 'Settings', icon: 'settings', active: state.tab === 'settings', onClick: () => navTo('settings') },
|
|
729
736
|
];
|
|
730
737
|
return WorkspaceRail({
|
|
@@ -928,9 +935,76 @@ function mainContent() {
|
|
|
928
935
|
if (state.tab === 'history') return historyMain();
|
|
929
936
|
if (state.tab === 'files') return filesMain();
|
|
930
937
|
if (state.tab === 'live') return liveMain();
|
|
938
|
+
if (state.tab === 'git') return gitMain();
|
|
931
939
|
return settingsMain();
|
|
932
940
|
}
|
|
933
941
|
|
|
942
|
+
// --- git (diff/log + worktree list) ---
|
|
943
|
+
// Minimal panel: loads git.log + worktree.list on entry, git.diff on demand.
|
|
944
|
+
// A richer visual component (GitDiffView/WorktreeSwitcher) is being built in
|
|
945
|
+
// anentrypoint-design in parallel; swap this for those exports once the
|
|
946
|
+
// vendored SDK bundle (site/app/vendor/anentrypoint-design) is rebuilt to
|
|
947
|
+
// include them - it does not yet (checked: no GitDiffView/WorktreeSwitcher
|
|
948
|
+
// in the vendored 247420.js as of this change).
|
|
949
|
+
async function loadGitPanel() {
|
|
950
|
+
state.git.loading = true; state.git.error = null; render();
|
|
951
|
+
try {
|
|
952
|
+
const [commits, worktrees] = await Promise.all([
|
|
953
|
+
B.gitLog(state.backend, { limit: 20 }),
|
|
954
|
+
B.worktreeList(state.backend, {}),
|
|
955
|
+
]);
|
|
956
|
+
state.git.commits = commits;
|
|
957
|
+
state.git.worktrees = worktrees;
|
|
958
|
+
} catch (e) {
|
|
959
|
+
state.git.error = e.message || 'Could not load git status.';
|
|
960
|
+
}
|
|
961
|
+
state.git.loading = false; render();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function loadGitDiff(file) {
|
|
965
|
+
state.git.diffLoading = true; state.git.diffError = null; render();
|
|
966
|
+
try {
|
|
967
|
+
const { diff } = await B.gitDiff(state.backend, { file: file || undefined });
|
|
968
|
+
state.git.diff = diff || '(no changes)';
|
|
969
|
+
state.git.file = file || '';
|
|
970
|
+
} catch (e) {
|
|
971
|
+
state.git.diffError = e.message || 'Could not load diff.';
|
|
972
|
+
}
|
|
973
|
+
state.git.diffLoading = false; render();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function gitMain() {
|
|
977
|
+
if (!state.git.loading && !state.git.commits.length && !state.git.worktrees.length && !state.git.error) loadGitPanel();
|
|
978
|
+
const g = state.git;
|
|
979
|
+
return [
|
|
980
|
+
PageHeader({ compact: true, dense: true, title: 'Git', lede: 'Working-tree diff, recent commits, and worktrees.' }),
|
|
981
|
+
g.error ? Alert({ key: 'git-err', kind: 'error', title: 'Git error', children: g.error }) : null,
|
|
982
|
+
Row({ key: 'git-actions', children: [
|
|
983
|
+
Btn({ key: 'refresh', onClick: () => loadGitPanel(), children: g.loading ? 'loading…' : 'refresh' }),
|
|
984
|
+
Btn({ key: 'diff-all', onClick: () => loadGitDiff(''), children: g.diffLoading ? 'loading diff…' : 'load full diff' }),
|
|
985
|
+
] }),
|
|
986
|
+
Panel({ id: 'git-diff', title: g.file ? ('diff: ' + g.file) : 'diff', children:
|
|
987
|
+
g.diffError ? h('p', { key: 'de', class: 't-meta field-error' }, g.diffError)
|
|
988
|
+
: h('pre', { key: 'dp', class: 'git-diff-pre' }, g.diff || '(no diff loaded)') }),
|
|
989
|
+
Panel({ id: 'git-log', title: 'recent commits', children:
|
|
990
|
+
!g.commits.length ? h('p', { key: 'nc', class: 't-meta' }, g.loading ? 'loading…' : 'no commits')
|
|
991
|
+
: h('ul', { key: 'cl', class: 'git-commit-list' }, g.commits.map(c =>
|
|
992
|
+
h('li', { key: c.hash }, [
|
|
993
|
+
h('code', { key: 'h' }, (c.hash || '').slice(0, 8)),
|
|
994
|
+
' ' + (c.subject || ''),
|
|
995
|
+
h('span', { key: 'm', class: 't-meta' }, ' — ' + (c.author || '') + ' · ' + (c.date || '')),
|
|
996
|
+
Btn({ key: 'view', small: true, onClick: () => loadGitDiff(''), children: 'view' }),
|
|
997
|
+
]))) }),
|
|
998
|
+
Panel({ id: 'git-worktrees', title: 'worktrees', children:
|
|
999
|
+
!g.worktrees.length ? h('p', { key: 'nw', class: 't-meta' }, g.loading ? 'loading…' : 'no worktrees')
|
|
1000
|
+
: h('ul', { key: 'wl', class: 'git-worktree-list' }, g.worktrees.map((w, i) =>
|
|
1001
|
+
h('li', { key: w.path || i }, [
|
|
1002
|
+
h('code', { key: 'p' }, w.path || ''),
|
|
1003
|
+
' ' + (w.branch || (w.detached ? '(detached)' : '')),
|
|
1004
|
+
]))) }),
|
|
1005
|
+
];
|
|
1006
|
+
}
|
|
1007
|
+
|
|
934
1008
|
// --- files (folder browser) ---
|
|
935
1009
|
async function loadDir(dirPath, { fromHash = false } = {}) {
|
|
936
1010
|
state.files = state.files || {};
|
package/site/app/js/backend.js
CHANGED
|
@@ -428,6 +428,30 @@ export async function listAgentModels(base, agentId) {
|
|
|
428
428
|
} catch { return []; }
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
// ---------- Git / worktrees (WS) ----------
|
|
432
|
+
|
|
433
|
+
export async function gitDiff(base, { cwd, file, staged } = {}) {
|
|
434
|
+
return wsCall(base, 'git.diff', { cwd, file, staged });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function gitLog(base, { cwd, limit } = {}) {
|
|
438
|
+
const { commits } = await wsCall(base, 'git.log', { cwd, limit });
|
|
439
|
+
return commits || [];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function worktreeList(base, { cwd } = {}) {
|
|
443
|
+
const { worktrees } = await wsCall(base, 'worktree.list', { cwd });
|
|
444
|
+
return worktrees || [];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export async function worktreeCreate(base, { cwd, path, branch, newBranch } = {}) {
|
|
448
|
+
return wsCall(base, 'worktree.create', { cwd, path, branch, newBranch });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export async function worktreeRemove(base, { cwd, path, force } = {}) {
|
|
452
|
+
return wsCall(base, 'worktree.remove', { cwd, path, force });
|
|
453
|
+
}
|
|
454
|
+
|
|
431
455
|
// ---------- Streaming chat (WS) ----------
|
|
432
456
|
//
|
|
433
457
|
// Yields events of shape:
|
|
@@ -4967,6 +4967,16 @@
|
|
|
4967
4967
|
.ds-247420 .ds-stat-val.err-rate { font-size: clamp(24px, 7cqi, 32px); color: var(--warn); }
|
|
4968
4968
|
.ds-247420 .ds-stat-lbl { font-size: var(--fs-micro); color: var(--fg-3); margin-top: 2px; }
|
|
4969
4969
|
|
|
4970
|
+
/* RateCell — tone-colored numeric table cell (success-rate / percentile-latency
|
|
4971
|
+
columns), ported from docstudio's endpointsView() success-rate coloring.
|
|
4972
|
+
Neutral at rest; color carries meaning only, matching Table's sortable-header
|
|
4973
|
+
restraint pattern. */
|
|
4974
|
+
.ds-247420 .ds-rate-cell { font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
4975
|
+
.ds-247420 .ds-rate-cell-neutral { color: var(--fg-2); font-weight: 400; }
|
|
4976
|
+
.ds-247420 .ds-rate-cell-good { color: var(--success); }
|
|
4977
|
+
.ds-247420 .ds-rate-cell-warn { color: var(--warn); }
|
|
4978
|
+
.ds-247420 .ds-rate-cell-bad { color: var(--danger); }
|
|
4979
|
+
|
|
4970
4980
|
/* Inline row — flex row with centered items and a small gap, for demo/kit
|
|
4971
4981
|
markup that needs to lay siblings out horizontally without an inline style. */
|
|
4972
4982
|
.ds-247420 .ds-inline-row { display: flex; align-items: center; gap: var(--space-2, 8px); }
|