mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
Adds packages/desktop, a small native app (Rust: tao, wry/WKWebView, muda, objc2) that opens Codeman in its own window. The universal app is about 5 MB (3.2 MB as a DMG) and runs on macOS 11.3 or later. - Connect screen with two tabs. This Mac finds the local Codeman and offers Open, Start or Install. Start uses launchd for a LaunchAgent install and codeman web -d otherwise. It never starts a second server while one runs, because two servers on one data dir share live sessions. Another machine connects by address (recent servers, Keychain logins) and can install Codeman there over SSH. - Installs run Codeman's install.sh in Terminal from a self-deleting .command script. The installer needs a terminal for sudo, Homebrew and ssh. A local install uses --local --service and the first free port from 3000, and the app opens Codeman once it answers. - The local Codeman's login is read from its LaunchAgent or ~/.codeman/.env. It is sent only to 127.0.0.1 on that port, only while the user's own Codeman process holds it, and is never saved. - Native JS dialogs (alert, confirm, prompt) and HTTP Basic challenge answers, both scoped to Codeman's own pages, so the page keeps working after a server restart. App cookies are cleared before every connect. - Pop-outs through window.CodemanHost, with an allowlist (webview/ never opens a window), external links in the default browser, downloads to ~/Downloads, real full screen and window state that is remembered. - The connect page refuses a WebKit older than Safari 16.4, which Codeman's terminal needs. Codeman side: viewSubagentTranscript shows a toast when the pop-up is refused instead of failing, with zh-CN strings and a test that pins them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.9 KiB
TypeScript
92 lines
3.9 KiB
TypeScript
/**
|
|
* @fileoverview zh-CN coverage for the toasts shown when a window could not be
|
|
* opened, and for the Codeman desktop app's own toasts.
|
|
*
|
|
* The literals are read from their sources: the transcript and detached
|
|
* preview toasts from panels-ui.js, and the desktop shim's toasts from
|
|
* packages/desktop/src/assets.rs (a Rust file whose strings reach Codeman's
|
|
* `showToast`), so a reworded toast on either side without an entry fails
|
|
* here. The download toasts carry a file name and go through the pattern
|
|
* rule, which must keep the name exactly as written.
|
|
*
|
|
* Port: N/A.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import vm from 'node:vm';
|
|
import { JSDOM } from 'jsdom';
|
|
import { afterAll, describe, expect, it } from 'vitest';
|
|
|
|
const PUBLIC = resolve(import.meta.dirname, '../src/web/public');
|
|
const read = (f: string) => readFileSync(resolve(PUBLIC, f), 'utf8');
|
|
const I18N = read('i18n.js');
|
|
const PANELS_UI = read('panels-ui.js');
|
|
const ASSETS_RS = readFileSync(resolve(import.meta.dirname, '../packages/desktop/src/assets.rs'), 'utf8');
|
|
|
|
type Api = { t(s: string): string; configure(o: Record<string, string>): void };
|
|
function translator(language: string) {
|
|
const dom = new JSDOM('<!doctype html><html><body></body></html>', {
|
|
runScripts: 'outside-only',
|
|
url: 'http://localhost/',
|
|
});
|
|
vm.runInContext(I18N, dom.getInternalVMContext(), { filename: 'i18n.js' });
|
|
const api = (dom.window as unknown as { CodemanI18n: Api }).CodemanI18n;
|
|
api.configure({ language });
|
|
return { api, dom };
|
|
}
|
|
const zh = translator('zh-CN');
|
|
const en = translator('en');
|
|
afterAll(() => {
|
|
zh.dom.window.close();
|
|
en.dom.window.close();
|
|
});
|
|
|
|
const panelToasts = [
|
|
...PANELS_UI.matchAll(/this\.showToast\('(Could not open the transcript window[^']*|Pop-up blocked[^']*)'/g),
|
|
].map((m) => m[1]);
|
|
// The shim's JavaScript may quote a literal either way.
|
|
const shimToasts = [...ASSETS_RS.matchAll(/showToast\((['"])([^'"]+)\1/g)].map((m) => m[2]);
|
|
const downloadTemplates = [...ASSETS_RS.matchAll(/format!\("([^"]*\{file_name\}[^"]*)"\)/g)].map((m) => m[1]);
|
|
|
|
// File names with spaces, dots, brackets and English words, one of them a
|
|
// dictionary word on its own ("Settings saved"), which must not be translated.
|
|
const FILE_NAMES = ['report v2.pdf', 'Settings saved', 'a (1).tar.gz', '数据.csv'];
|
|
|
|
describe('the window and desktop toasts in zh-CN', () => {
|
|
it('finds the strings it checks (the check is not vacuous)', () => {
|
|
expect(new Set(panelToasts)).toEqual(
|
|
new Set([
|
|
'Could not open the transcript window: pop-up windows are blocked here',
|
|
'Pop-up blocked: allow pop-ups for this site to detach previews',
|
|
])
|
|
);
|
|
expect(new Set(shimToasts)).toEqual(new Set(['Could not open a new window']));
|
|
expect(new Set(downloadTemplates)).toEqual(
|
|
new Set(['Downloaded {file_name} to Downloads', 'Download of {file_name} failed'])
|
|
);
|
|
});
|
|
|
|
it('every literal toast translates, and reads the same in English', () => {
|
|
const bad = [...panelToasts, ...shimToasts].filter((l) => {
|
|
const text = zh.api.t(l);
|
|
return text === l || /[A-Za-z]/.test(text) || en.api.t(l) !== l;
|
|
});
|
|
expect(bad).toEqual([]);
|
|
});
|
|
|
|
it('the download toasts translate around the file name, which stays as written', () => {
|
|
for (const template of downloadTemplates) {
|
|
for (const name of FILE_NAMES) {
|
|
const source = template.replace('{file_name}', name);
|
|
const text = zh.api.t(source);
|
|
expect(text, source).not.toBe(source);
|
|
expect(text.split(name), source).toHaveLength(2);
|
|
expect(text.replace(name, ''), source).not.toMatch(/[A-Za-z]/);
|
|
expect(en.api.t(source)).toBe(source);
|
|
}
|
|
}
|
|
expect(zh.api.t('Downloaded report v2.pdf to Downloads')).toBe('已将 report v2.pdf 下载到“下载”文件夹');
|
|
expect(zh.api.t('Download of report v2.pdf failed')).toBe('下载 report v2.pdf 失败');
|
|
});
|
|
});
|