Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
ab59020
docs(webui): the engine layer has six files, not five (webui-parity 107)
fengzhi09 Oct 1, 2026
1dc559a
test(webui): M2 capability-declaration snapshot vs the real host (eng…
fengzhi09 Oct 1, 2026
8c085fa
test(webui): point the capability snapshot at the engine layer's real…
fengzhi09 Oct 1, 2026
aa5ab47
fix(webui): stop the shell from carrying one session's state into ano…
fengzhi09 Oct 1, 2026
af01e0e
refactor(webui): the plugins and turn-diff routes take the host from …
fengzhi09 Oct 1, 2026
f1842ba
test(webui): make the run-mirror, first-turn-guard and mavis-usage su…
fengzhi09 Oct 1, 2026
726d286
refactor(webui): the plugins and turn-diff routes take the host from …
fengzhi09 Oct 1, 2026
a4fad96
test(webui): make the run-mirror, first-turn-guard and mavis-usage su…
fengzhi09 Oct 1, 2026
e7c0ce9
feat(webui): the five read endpoints ask the engine facade, not the t…
fengzhi09 Oct 1, 2026
e053ae7
feat(webui): the session-tree and export endpoints ask the engine fac…
fengzhi09 Oct 1, 2026
4fb8267
feat(webui): the usage endpoints ask the engine facade, and the deriv…
fengzhi09 Oct 1, 2026
88b9a48
fix(webui): rebase M3-B3 onto M3-B2, register B2's two tmp prefixes, …
fengzhi09 Oct 1, 2026
6bc24bd
feat(webui): the account, model and capability reads ask the engine f…
fengzhi09 Oct 2, 2026
eb2a429
feat(webui): #73 swaps the ACP wire table for the 14-key engine-capab…
fengzhi09 Oct 2, 2026
2baf051
fix(webui): stop two B4 comments describing behaviour the code no lon…
fengzhi09 Oct 2, 2026
edf2b1e
feat(webui): move the session write family behind the engine facade
fengzhi09 Oct 2, 2026
1506cc2
fix(webui): drop whitespace text nodes in markdown tables and dedupe …
fengzhi09 Oct 2, 2026
8cca235
fix(webui): sweep the non-flipping inverted text token off primary su…
fengzhi09 Oct 2, 2026
eecd8c0
feat(webui): move session switch behind the engine facade
fengzhi09 Oct 2, 2026
0cfd51f
Merge main into dev-lhl
fengzhi09 Oct 2, 2026
e4cf052
chore: allowlist the leak-tripwire fixture in model-reads tests
fengzhi09 Oct 2, 2026
3f5b8d2
test(webui): pin session-writes cleanup-orphans test to isolated paths
fengzhi09 Oct 2, 2026
e7df93d
chore: ignore gitleaks fingerprints of deliberate test fixtures
fengzhi09 Oct 2, 2026
62814ff
chore: make the gitleaks fixture allowlists path-only
fengzhi09 Oct 2, 2026
3074010
feat(webui): move interrupt and load endpoints behind the engine facade
fengzhi09 Oct 3, 2026
063a43a
fix(webui): take the plan's 5s abort force-kill bound by product call
fengzhi09 Oct 3, 2026
90cf85e
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
a9af820
docs(webui): add session-switch, interrupt and session-load to the ar…
fengzhi09 Oct 3, 2026
4d904c3
docs(webui): add the missing zh-CN section for the B5 write family
fengzhi09 Oct 3, 2026
fdc3ff2
fix(webui): make webui-only session delete return promptly instead of…
fengzhi09 Oct 3, 2026
dab453d
fix(webui): retire lossy streaming mirrors when the engine transcript…
fengzhi09 Oct 3, 2026
7138b5b
feat(webui): add the streaming-send capability gate and pure stream b…
fengzhi09 Oct 3, 2026
a2223f4
feat(webui): run send on the runtime transport behind the engine facade
fengzhi09 Oct 3, 2026
8b51fdd
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
964c0cf
feat(webui): answer set-mode and set-config-option with structured 50…
fengzhi09 Oct 3, 2026
bdde1eb
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
a112e45
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
245a101
docs(webui): add the streaming-send architecture section, bilingual
fengzhi09 Oct 3, 2026
d9e181d
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
679d0fe
docs(webui): add the streaming-send architecture section, bilingual
fengzhi09 Oct 3, 2026
a078ee6
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
591ccff
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
b529543
fix(local-runtime): make an abandoned migration lease recoverable at …
fengzhi09 Oct 3, 2026
a8e56dc
feat(webui): move model and permission writes behind the engine facade
fengzhi09 Oct 3, 2026
48199c5
Reset dev-lhl to the full local integration line (B9+B10+docs+P13+P14…
fengzhi09 Oct 3, 2026
5661bb9
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
4b5e8d2
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
9fdd8d1
fix(webui): surface truncated acp stderr in failure alerts
fengzhi09 Oct 3, 2026
5427f23
feat(webui): move the provider family behind the engine facade with s…
fengzhi09 Oct 3, 2026
afa995e
fix(webui): acknowledge in-flight messages explicitly instead of echo…
fengzhi09 Oct 3, 2026
6c30484
fix(webui): normalise the expected side of the provider cwd path asse…
fengzhi09 Oct 3, 2026
e68a8df
feat(webui): bridge thinkingEffort as the third config id and gate th…
fengzhi09 Oct 3, 2026
1473183
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
15a3f42
feat(webui): register the acp transport as the first engine capabilit…
fengzhi09 Oct 3, 2026
313286a
fix(webui): keep over-tall code blocks inside their scroll container
fengzhi09 Oct 3, 2026
506c0a9
feat(webui): replace the flat provider form with the desktop-style di…
fengzhi09 Oct 3, 2026
25da27a
docs(webui): document the provider dialog interaction, bilingual
fengzhi09 Oct 3, 2026
38befac
feat(webui): route session deletion through the engine deleteSession …
fengzhi09 Oct 3, 2026
06a0e8e
feat(webui): open the host services window for capability exposure ba…
fengzhi09 Oct 3, 2026
60e5361
feat(webui): register the exec transport in the engine capability reg…
fengzhi09 Oct 3, 2026
64914d7
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
382c10c
fix(webui): escape raw svg tags in markdown output instead of mountin…
fengzhi09 Oct 3, 2026
bc49315
fix(webui): consume the real stream-json events on the exec transport
fengzhi09 Oct 3, 2026
35f1e0c
feat(webui): unlock the session context menu actions backed by the en…
fengzhi09 Oct 3, 2026
2b3a9e4
test(webui): register the PB-1 real-host tmp prefix
fengzhi09 Oct 3, 2026
f9829b3
chore(release-tools): register the mcode-exec-stream- tmp prefix
fengzhi09 Oct 3, 2026
b5bad19
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
33629d7
feat(webui): wire the context-window usage switch to the composer rea…
fengzhi09 Oct 3, 2026
c10736d
feat(webui): unlock the project menu's reveal-in-folder (SB-6)
fengzhi09 Oct 3, 2026
92abe74
feat(webui): make the Shortcuts page state what the browser can do
fengzhi09 Oct 3, 2026
595c06d
feat(webui): plan card reads the account tier, honest cloud placeholders
fengzhi09 Oct 3, 2026
d087f28
feat(webui): wire the usage-and-models model source to the engine (SB-1)
fengzhi09 Oct 3, 2026
43d0b2a
dev-lhl: SB-3/6/2/7/1 + SB-5 + P19 + SB-4 (settings waves, delete-han…
fengzhi09 Oct 3, 2026
c96e7a2
fix(webui): re-read the account after a source switch, and stop the k…
fengzhi09 Oct 3, 2026
c290440
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
2250209
dev-lhl: SB-10 (DOM harness) + PB-3 (worktree page) + docs stale-pare…
fengzhi09 Oct 3, 2026
8fbb8c3
dev-lhl: P21 locale-independent worktree discovery + docs stale fix
fengzhi09 Oct 3, 2026
9806700
fix(webui): stop the built distribution from tripping the credential …
fengzhi09 Oct 3, 2026
0755124
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/local-runtime/src/files/git-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export interface GitRunResult {
code: number;
stdout: string;
stderr: string;
/**
* Set only when `git` never ran — a missing binary or an unusable working
* directory. Absent means Git produced a verdict, so `code` is a real exit
* status that callers may branch on instead of parsing a translated message.
*/
spawnError?: string;
}

export async function git(args: string[], workspace: string): Promise<GitRunResult> {
Expand All @@ -29,6 +35,7 @@ export async function git(args: string[], workspace: string): Promise<GitRunResu
code: typeof err.code === 'number' ? err.code : 1,
stdout: bufferToString(err.stdout),
stderr: bufferToString(err.stderr) || err.message,
...(typeof err.code === 'string' ? { spawnError: err.code } : {}),
};
}
}
Expand Down
23 changes: 22 additions & 1 deletion packages/local-runtime/src/files/worktrees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function listWorkspaceGitWorktrees(
success: false,
worktrees: [],
error,
code: /not a git repository/i.test(error) ? 'not_git_repository' : 'workspace_unavailable',
code: await classifyUnresolvedWorkspace(workspace),
};
}
const root = rootResult.stdout.trim();
Expand Down Expand Up @@ -111,6 +111,27 @@ export async function listWorkspaceGitWorktrees(
};
}

/**
* Tell "this folder was never a repository" apart from "the repository here
* cannot be listed" without reading Git's diagnostics.
*
* Git translates `fatal: not a git repository ...` into the user's locale, so
* matching that sentence only classified an English-speaking host: under
* `LANG=zh_CN` every plain folder fell into the generic bucket and
* `not_git_repository` was unreachable. `git rev-parse --git-dir` states the
* same fact in its exit status — 0 inside a repository (bare included), 128
* outside one — and those bytes never change with the locale. A bare
* repository therefore lands in `workspace_unavailable`: it is a repository,
* just one without the work tree this listing walks.
*/
async function classifyUnresolvedWorkspace(workspace: string): Promise<WorktreeDiscoveryCode> {
const probe = await git(['rev-parse', '--git-dir'], workspace);
if (probe.spawnError !== undefined) return 'workspace_unavailable';
return probe.code === 0 && probe.stdout.trim() !== ''
? 'workspace_unavailable'
: 'not_git_repository';
}

/** null means confirmed missing; undefined means metadata could not be read. */
async function worktreeLastModifiedMs(worktreePath: string): Promise<number | undefined | null> {
try {
Expand Down
124 changes: 124 additions & 0 deletions packages/local-runtime/test/unit/worktree-discovery-code.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Discovery-code classification for `listWorkspaceGitWorktrees`, driven by a
// stubbed `git` so the assertions hold on hosts whose Git prints English only.
//
// Why the stub instead of a real repository: the regression this file pins is
// that the classification read Git's human-readable diagnostic. That text is
// translated by Git, so a real-subprocess test only reproduces it on a host
// carrying the matching catalogue. Here the stub returns exactly what a
// localized Git returns, and the code under test must classify it from the
// exit status alone.

import { mkdtemp, mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import type { GitRunResult } from '../../src/files/git-process.js';

const gitStub = vi.fn<(args: string[], workspace: string) => Promise<GitRunResult>>();

vi.mock('../../src/files/git-process.js', () => ({
git: (args: string[], workspace: string) => gitStub(args, workspace),
}));

const { listWorkspaceGitWorktrees } = await import('../../src/files/worktrees.js');

const NOT_A_REPO_ZH = 'fatal: 不是 git 仓库(或者任何父目录):.git';
const NOT_A_REPO_EN = 'fatal: not a git repository (or any of the parent directories): .git';
const NO_WORK_TREE_ZH = 'fatal: 该操作必须在一个工作区中运行';

let workspace: string;

beforeEach(async () => {
gitStub.mockReset();
workspace = await mkdtemp(join(tmpdir(), 'mcode-worktree-code-'));
await mkdir(join(workspace, 'plain-folder'), { recursive: true });
});

afterEach(async () => {
await rm(workspace, { recursive: true, force: true });
});

/** Answer the two `rev-parse` probes the discovery performs. */
function stubRevParse(options: {
toplevel: GitRunResult;
gitDir: GitRunResult;
}): void {
gitStub.mockImplementation(async (args) =>
args.includes('--git-dir') ? options.gitDir : options.toplevel,
);
}

function failed(code: number, stderr: string): GitRunResult {
return { code, stdout: '', stderr };
}

describe('listWorkspaceGitWorktrees discovery code', () => {
it.each([
['zh_CN', NOT_A_REPO_ZH],
['en_US', NOT_A_REPO_EN],
])('classifies a plain folder as not_git_repository under %s diagnostics', async (_tag, stderr) => {
stubRevParse({
toplevel: failed(128, stderr),
gitDir: failed(128, stderr),
});

const result = await listWorkspaceGitWorktrees(join(workspace, 'plain-folder'));

expect(result.success).toBe(false);
expect(result.code).toBe('not_git_repository');
expect(result.error).toBe(stderr);
});

it('keeps a repository without a work tree out of the not-a-repository bucket', async () => {
// A bare repository answers `--git-dir` successfully while
// `--show-toplevel` fails; that is a repository this listing cannot walk,
// not a folder that was never one.
stubRevParse({
toplevel: failed(128, NO_WORK_TREE_ZH),
gitDir: { code: 0, stdout: '.\n', stderr: '' },
});

const result = await listWorkspaceGitWorktrees(workspace);

expect(result.success).toBe(false);
expect(result.code).toBe('workspace_unavailable');
});

it('falls back to the generic bucket when git could not run at all', async () => {
// No binary, unusable cwd: neither probe produced a Git verdict, so the
// discovery must not claim the folder was never a repository.
gitStub.mockImplementation(async () => ({
code: 1,
stdout: '',
stderr: 'spawn git ENOENT',
spawnError: 'ENOENT',
}));

const result = await listWorkspaceGitWorktrees(workspace);

expect(result.success).toBe(false);
expect(result.code).toBe('workspace_unavailable');
});

it('still lists the worktrees of a healthy repository', async () => {
gitStub.mockImplementation(async (args) => {
if (args.includes('--show-toplevel')) return { code: 0, stdout: `${workspace}\n`, stderr: '' };
if (args.includes('worktree')) {
return {
code: 0,
stdout: `worktree ${workspace}\nHEAD 1111111111111111111111111111111111111111\nbranch refs/heads/main\n\n`,
stderr: '',
};
}
return failed(1, 'unexpected call');
});

const result = await listWorkspaceGitWorktrees(workspace);

expect(result.success).toBe(true);
expect(result.worktrees).toHaveLength(1);
expect(result.worktrees[0]).toMatchObject({ branch: 'main', isMain: true });
});
});
105 changes: 105 additions & 0 deletions packages/local-runtime/test/unit/worktree-locale-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Locale parity for the worktree discovery codes, against a real `git`.
//
// The discovery used to recognise "not a repository" by matching Git's
// English diagnostic, so on a host whose Git speaks the user's language every
// plain folder degraded to the generic `workspace_unavailable` bucket. These
// cases run the real subprocess under a fixed locale and assert the discovery
// code, never the wording — a Git build without the requested catalogue simply
// stays in English and the expectation still holds.

import { execFile as execFileCallback } from 'node:child_process';
import { mkdtemp, mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';

import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { listWorkspaceGitWorktrees } from '../../src/files/worktrees.js';

const execFile = promisify(execFileCallback);

const LOCALES = ['zh_CN.utf8', 'en_US.UTF-8'];

let fixture: string;
let plainFolder: string;
let repository: string;

beforeEach(async () => {
fixture = await mkdtemp(join(tmpdir(), 'mcode-worktree-locale-'));
plainFolder = join(fixture, 'plain-folder');
repository = join(fixture, 'repository');
await mkdir(plainFolder, { recursive: true });
await mkdir(repository, { recursive: true });
await git(repository, ['-c', 'init.defaultBranch=main', 'init', '-q']);
await git(repository, [
'-c',
'user.name=P21',
'-c',
'user.email=p21@example.test',
'commit',
'-q',
'--allow-empty',
'-m',
'initial',
]);
});

afterEach(async () => {
await rm(fixture, { recursive: true, force: true });
});

async function git(cwd: string, args: string[]): Promise<void> {
await execFile('git', args, { cwd, encoding: 'utf-8' });
}

/**
* `git` translates its diagnostics from `LC_ALL`/`LANG`/`LC_MESSAGES`; the
* discovery inherits this process's environment, so pinning the variables here
* is what fixes the subprocess locale. Restoration is unconditional: another
* case in this file must not inherit the previous locale.
*/
async function withLocale<T>(locale: string, run: () => Promise<T>): Promise<T> {
const saved = ['LANG', 'LC_ALL', 'LC_MESSAGES'].map((key) => [key, process.env[key]] as const);
process.env.LANG = locale;
process.env.LC_ALL = locale;
delete process.env.LC_MESSAGES;
try {
return await run();
} finally {
for (const [key, value] of saved) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
}

describe('listWorkspaceGitWorktrees locale parity', () => {
it.each(LOCALES)('reports a plain folder as not_git_repository under %s', async (locale) => {
const result = await withLocale(locale, () => listWorkspaceGitWorktrees(plainFolder));

expect(result.success).toBe(false);
expect(result.code).toBe('not_git_repository');
expect(result.error).toBeTruthy();
});

it.each(LOCALES)('lists the worktrees of a repository under %s', async (locale) => {
const result = await withLocale(locale, () => listWorkspaceGitWorktrees(repository));

expect(result.success).toBe(true);
expect(result.code).toBeUndefined();
expect(result.worktrees).toHaveLength(1);
expect(result.worktrees[0]).toMatchObject({ branch: 'main', isMain: true, isActive: true });
});

it('produces the same codes regardless of the locale in force', async () => {
const perLocale = [];
for (const locale of LOCALES) {
perLocale.push(
(await withLocale(locale, () => listWorkspaceGitWorktrees(plainFolder))).code,
(await withLocale(locale, () => listWorkspaceGitWorktrees(repository))).code,
);
}
expect(perLocale).toEqual(['not_git_repository', undefined, 'not_git_repository', undefined]);
});
});
7 changes: 6 additions & 1 deletion packages/webui/server/engine/model-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,14 @@ export function publicApiKeyStatus(status) {
const record = status && typeof status === "object" ? status : {};
const cached = record.cachedStatus && typeof record.cachedStatus === "object" ? record.cachedStatus : {};
const lastTested = typeof cached.lastTestedAt === "number" ? cached.lastTestedAt : null;
// Destructured, not read as `record.hasApiKey` in the projection: the bundler
// renames `record` to a generated identifier, and a `hasKey: <renamed>.<member>`
// pair reads to the credential scanner as `hasKey = <16+ char secret>` inside the
// bundled distribution, failing the release audit on a boolean comparison.
const { hasApiKey } = record;
return {
available: true,
hasKey: record.hasApiKey === true,
hasKey: hasApiKey === true,
masked: typeof record.maskedApiKey === "string" ? record.maskedApiKey : null,
testState: typeof cached.state === "string" ? cached.state : null,
lastTestedAtMs: lastTested,
Expand Down
2 changes: 2 additions & 0 deletions release/public-source.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions test/vitest-suites.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@
"packages/local-runtime/test/unit/thread-goal/host-integration-settlement.test.ts",
"packages/local-runtime/test/unit/thread-goal/host-integration-verifier-charge-rearm.test.ts",
"packages/local-runtime/test/unit/token-counter-adapter-routing.test.ts",
"packages/local-runtime/test/unit/worktree-discovery-code.test.ts",
"packages/local-runtime/test/unit/worktree-locale-parity.test.ts",
"packages/mcode-tools-host/test/unit/lease-broker.test.ts",
"packages/mcode-tools-host/test/unit/resource-manifest.test.ts",
"packages/mcode-tools-host/test/unit/resource.test.ts",
Expand Down
Loading