@baique/pi-web-sky 0.1.35 → 0.1.36
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +16 -16
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +16 -16
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +10 -5
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
|
|
5
|
+
const jiti = createJiti(import.meta.url);
|
|
6
|
+
const {
|
|
7
|
+
getLocalePlugin,
|
|
8
|
+
getSupportedLocales,
|
|
9
|
+
registerLocale,
|
|
10
|
+
resolveBrowserLocale,
|
|
11
|
+
} = await jiti.import("./registry.ts");
|
|
12
|
+
|
|
13
|
+
test("uses the first supported browser language and falls back to English", () => {
|
|
14
|
+
assert.equal(resolveBrowserLocale(["zh-CN", "en-US"]), "zh-CN");
|
|
15
|
+
assert.equal(resolveBrowserLocale(["zh", "en-US"]), "zh-CN");
|
|
16
|
+
assert.equal(resolveBrowserLocale(["en-US", "zh-CN"]), "en");
|
|
17
|
+
assert.equal(resolveBrowserLocale(["fr-FR", "zh-CN"]), "zh-CN");
|
|
18
|
+
assert.equal(resolveBrowserLocale(["fr-FR"]), "en");
|
|
19
|
+
assert.equal(resolveBrowserLocale([]), "en");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("returns only registered locales", () => {
|
|
23
|
+
assert.deepEqual(getSupportedLocales(), ["en", "zh-CN"]);
|
|
24
|
+
assert.equal(getLocalePlugin("en").id, "en");
|
|
25
|
+
assert.equal(getLocalePlugin("missing"), undefined);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("allows a new locale plugin and rejects duplicate ids", () => {
|
|
29
|
+
registerLocale({ id: "test", label: "Test", messages: { "common.ok": "OK" } });
|
|
30
|
+
assert.equal(getLocalePlugin("test")?.label, "Test");
|
|
31
|
+
assert.throws(() => registerLocale({ id: "test", label: "Again", messages: {} }));
|
|
32
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { enLocale } from "./messages/en";
|
|
2
|
+
import { zhCNLocale } from "./messages/zh-CN";
|
|
3
|
+
import type { Locale, LocalePlugin } from "./types";
|
|
4
|
+
|
|
5
|
+
const localePlugins = new Map<string, LocalePlugin>();
|
|
6
|
+
|
|
7
|
+
/** 注册一个语言包;重复注册会抛出异常,避免静默覆盖翻译。 */
|
|
8
|
+
export function registerLocale(plugin: LocalePlugin): void {
|
|
9
|
+
if (!plugin.id.trim()) throw new Error("Locale id must not be empty");
|
|
10
|
+
if (localePlugins.has(plugin.id)) throw new Error(`Locale already registered: ${plugin.id}`);
|
|
11
|
+
localePlugins.set(plugin.id, plugin);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 根据标识获取已注册的语言包。
|
|
16
|
+
* @param id 要查询的语言标识
|
|
17
|
+
* @returns 已注册的语言包,不存在时返回 undefined
|
|
18
|
+
*/
|
|
19
|
+
export function getLocalePlugin(id: string): LocalePlugin | undefined {
|
|
20
|
+
return localePlugins.get(id);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 获取当前已注册语言的稳定顺序列表。 */
|
|
24
|
+
export function getSupportedLocales(): string[] {
|
|
25
|
+
return [...localePlugins.keys()];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 将浏览器语言列表解析为 Pi Web 内置语言。
|
|
30
|
+
* @param languages 浏览器按优先级排列的语言列表
|
|
31
|
+
* @returns 匹配的内置语言,无法匹配时返回英语
|
|
32
|
+
*/
|
|
33
|
+
export function resolveBrowserLocale(languages: readonly string[]): Locale {
|
|
34
|
+
for (const language of languages) {
|
|
35
|
+
const normalized = language.toLowerCase();
|
|
36
|
+
if (normalized === "en" || normalized.startsWith("en-")) return "en";
|
|
37
|
+
if (normalized === "zh" || normalized.startsWith("zh-")) return "zh-CN";
|
|
38
|
+
}
|
|
39
|
+
return "en";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
registerLocale(enLocale);
|
|
43
|
+
registerLocale(zhCNLocale);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 内置的可用界面语言。 */
|
|
2
|
+
export type Locale = "en" | "zh-CN";
|
|
3
|
+
|
|
4
|
+
/** 翻译字符串使用的简单插值参数。 */
|
|
5
|
+
export type TranslationParams = Record<string, string | number>;
|
|
6
|
+
|
|
7
|
+
/** 可注册的语言包定义。 */
|
|
8
|
+
export interface LocalePlugin {
|
|
9
|
+
/** 语言包唯一标识。 */
|
|
10
|
+
id: string;
|
|
11
|
+
/** 用于语言选择菜单的显示名称。 */
|
|
12
|
+
label: string;
|
|
13
|
+
/** 以稳定 key 索引的翻译消息。 */
|
|
14
|
+
messages: Record<string, string>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
async function loadSubject() {
|
|
5
|
+
return import("./image-attachments.ts");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const image = { type: "image", mimeType: "image/png", data: "YWJj" };
|
|
9
|
+
|
|
10
|
+
test("calculates padded base64 byte lengths and rejects invalid data", async () => {
|
|
11
|
+
const { getBase64DecodedByteLength } = await loadSubject();
|
|
12
|
+
|
|
13
|
+
assert.equal(getBase64DecodedByteLength("YQ=="), 1);
|
|
14
|
+
assert.equal(getBase64DecodedByteLength("YWI="), 2);
|
|
15
|
+
assert.equal(getBase64DecodedByteLength("YWJj"), 3);
|
|
16
|
+
assert.equal(getBase64DecodedByteLength("not base64!"), null);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("rejects invalid, oversized, and too many image attachments", async () => {
|
|
20
|
+
const { MAX_ATTACHED_IMAGE_BYTES, MAX_ATTACHED_IMAGES, validateAgentImages } = await loadSubject();
|
|
21
|
+
const oversizedData = "AAAA".repeat(Math.ceil((MAX_ATTACHED_IMAGE_BYTES + 1) / 3));
|
|
22
|
+
|
|
23
|
+
assert.equal(validateAgentImages([image]), null);
|
|
24
|
+
assert.match(validateAgentImages([{ ...image, mimeType: "text/plain" }]), /valid base64 image/);
|
|
25
|
+
assert.match(validateAgentImages([{ ...image, data: oversizedData }]), /10MB/);
|
|
26
|
+
assert.match(validateAgentImages(Array.from({ length: MAX_ATTACHED_IMAGES + 1 }, () => image)), /at most/);
|
|
27
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const MAX_ATTACHED_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
2
|
+
export const MAX_ATTACHED_IMAGES = 10;
|
|
3
|
+
|
|
4
|
+
export interface Base64ImageAttachment {
|
|
5
|
+
data: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isBase64DataChar(code: number): boolean {
|
|
10
|
+
return (code >= 0x41 && code <= 0x5a)
|
|
11
|
+
|| (code >= 0x61 && code <= 0x7a)
|
|
12
|
+
|| (code >= 0x30 && code <= 0x39)
|
|
13
|
+
|| code === 0x2b
|
|
14
|
+
|| code === 0x2f;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getBase64DecodedByteLength(data: string): number | null {
|
|
18
|
+
if (!data || data.length % 4 !== 0) return null;
|
|
19
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
20
|
+
const dataEnd = data.length - padding;
|
|
21
|
+
for (let index = 0; index < dataEnd; index += 1) {
|
|
22
|
+
if (!isBase64DataChar(data.charCodeAt(index))) return null;
|
|
23
|
+
}
|
|
24
|
+
for (let index = dataEnd; index < data.length; index += 1) {
|
|
25
|
+
if (data[index] !== "=") return null;
|
|
26
|
+
}
|
|
27
|
+
return (data.length / 4) * 3 - padding;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isBase64ImageWithinLimits(value: unknown): value is Base64ImageAttachment {
|
|
31
|
+
if (!value || typeof value !== "object") return false;
|
|
32
|
+
const image = value as Partial<Base64ImageAttachment>;
|
|
33
|
+
if (typeof image.data !== "string" || typeof image.mimeType !== "string" || !image.mimeType.startsWith("image/")) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const bytes = getBase64DecodedByteLength(image.data);
|
|
37
|
+
return bytes !== null && bytes <= MAX_ATTACHED_IMAGE_BYTES;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Return an API-safe error for prompt, steering, and follow-up image arrays. */
|
|
41
|
+
export function validateAgentImages(value: unknown): string | null {
|
|
42
|
+
if (value === undefined) return null;
|
|
43
|
+
if (!Array.isArray(value)) return "images must be an array";
|
|
44
|
+
if (value.length > MAX_ATTACHED_IMAGES) {
|
|
45
|
+
return `A message can include at most ${MAX_ATTACHED_IMAGES} images`;
|
|
46
|
+
}
|
|
47
|
+
for (const image of value) {
|
|
48
|
+
if (!image || typeof image !== "object" || (image as { type?: unknown }).type !== "image") {
|
|
49
|
+
return "Each attachment must be an image";
|
|
50
|
+
}
|
|
51
|
+
if (!isBase64ImageWithinLimits(image)) {
|
|
52
|
+
return `Each image must be valid base64 image data of ${MAX_ATTACHED_IMAGE_BYTES / (1024 * 1024)}MB or smaller`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
async function loadSubject() {
|
|
5
|
+
return import("./initial-navigation.ts");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
test("uses cwd instead of session when both parameters are present", async () => {
|
|
9
|
+
const { getInitialNavigation } = await loadSubject();
|
|
10
|
+
const result = getInitialNavigation(new URLSearchParams({
|
|
11
|
+
cwd: " /work/project ",
|
|
12
|
+
session: "saved-session",
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
assert.deepEqual(result, {
|
|
16
|
+
requestedCwd: "/work/project",
|
|
17
|
+
sessionId: null,
|
|
18
|
+
boardId: null,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("restores session when cwd is absent", async () => {
|
|
23
|
+
const { getInitialNavigation } = await loadSubject();
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(
|
|
26
|
+
getInitialNavigation(new URLSearchParams({ session: "saved-session" })),
|
|
27
|
+
{ requestedCwd: null, sessionId: "saved-session", boardId: null },
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("treats an empty cwd as absent", async () => {
|
|
32
|
+
const { getInitialNavigation } = await loadSubject();
|
|
33
|
+
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
getInitialNavigation(new URLSearchParams({ cwd: " ", session: "saved-session" })),
|
|
36
|
+
{ requestedCwd: null, sessionId: "saved-session", boardId: null },
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("preserves a URL-encoded Windows path", async () => {
|
|
41
|
+
const { getInitialNavigation } = await loadSubject();
|
|
42
|
+
|
|
43
|
+
assert.deepEqual(
|
|
44
|
+
getInitialNavigation(new URLSearchParams("cwd=C%3A%5CProjects%5Cpi-web")),
|
|
45
|
+
{ requestedCwd: "C:\\Projects\\pi-web", sessionId: null, boardId: null },
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("restores board from ?board= param", async () => {
|
|
50
|
+
const { getInitialNavigation } = await loadSubject();
|
|
51
|
+
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
getInitialNavigation(new URLSearchParams({ board: "my-board-id" })),
|
|
54
|
+
{ requestedCwd: null, sessionId: null, boardId: "my-board-id" },
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("board param does not interfere with cwd", async () => {
|
|
59
|
+
const { getInitialNavigation } = await loadSubject();
|
|
60
|
+
|
|
61
|
+
assert.deepEqual(
|
|
62
|
+
getInitialNavigation(new URLSearchParams({ cwd: "/work", board: "b1" })),
|
|
63
|
+
{ requestedCwd: "/work", sessionId: null, boardId: "b1" },
|
|
64
|
+
);
|
|
65
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface InitialNavigation {
|
|
2
|
+
requestedCwd: string | null;
|
|
3
|
+
sessionId: string | null;
|
|
4
|
+
boardId: string | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getInitialNavigation(searchParams: Pick<URLSearchParams, "get">): InitialNavigation {
|
|
8
|
+
const requestedCwd = searchParams.get("cwd")?.trim() || null;
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
requestedCwd,
|
|
12
|
+
sessionId: requestedCwd ? null : searchParams.get("session"),
|
|
13
|
+
boardId: searchParams.get("board")?.trim() || null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { normalizeDisplayMath } from "./markdown.ts";
|
|
5
|
+
|
|
6
|
+
describe("normalizeDisplayMath", () => {
|
|
7
|
+
describe("single-line $$…$$", () => {
|
|
8
|
+
it("splits a single-line block into three lines", () => {
|
|
9
|
+
assert.equal(normalizeDisplayMath("$$a + b$$"), "$$\na + b\n$$");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("preserves the indent of the surrounding list item", () => {
|
|
13
|
+
assert.equal(
|
|
14
|
+
normalizeDisplayMath("- item:\n $$a + b$$\n- next"),
|
|
15
|
+
"- item:\n $$\n a + b\n $$\n- next",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("multi-line blocks with glued delimiters", () => {
|
|
21
|
+
it("moves a glued opening delimiter to its own line", () => {
|
|
22
|
+
const input = "$$\n\\frac{a}{b} = c\n<d$$\n\nafter";
|
|
23
|
+
assert.equal(normalizeDisplayMath(input), "$$\n\\frac{a}{b} = c\n<d\n$$\n\nafter");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("moves a glued closing delimiter to its own line", () => {
|
|
27
|
+
const input = "$$\nx = y\nz = w$$\n\nafter";
|
|
28
|
+
assert.equal(normalizeDisplayMath(input), "$$\nx = y\nz = w\n$$\n\nafter");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("blocks nested in GFM list items", () => {
|
|
33
|
+
it("re-indents lazy content lines of an indented bare-fence block", () => {
|
|
34
|
+
const input = "- item:\n $$\nx = y\n $$\n- next";
|
|
35
|
+
assert.equal(normalizeDisplayMath(input), "- item:\n $$\n x = y\n $$\n- next");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("re-indents partially indented content lines", () => {
|
|
39
|
+
const input = "- item:\n $$\n x = y\n $$\n- next";
|
|
40
|
+
assert.equal(normalizeDisplayMath(input), "- item:\n $$\n x = y\n $$\n- next");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not use a sibling list item's formula as a closing fence", () => {
|
|
44
|
+
const input = "- first\n $$x = y\n- second\n $$z = w$$\n- third";
|
|
45
|
+
assert.equal(
|
|
46
|
+
normalizeDisplayMath(input),
|
|
47
|
+
"- first\n $$x = y\n- second\n $$\n z = w\n $$\n- third",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not scan a bare fence past a sibling list item", () => {
|
|
52
|
+
const input = "- first\n $$\nx = y\n- second\n $$z = w$$\n- third";
|
|
53
|
+
assert.equal(
|
|
54
|
+
normalizeDisplayMath(input),
|
|
55
|
+
"- first\n $$\nx = y\n- second\n $$\n z = w\n $$\n- third",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("blocks that must stay untouched", () => {
|
|
61
|
+
it("leaves a top-level block with detached delimiters untouched", () => {
|
|
62
|
+
const input = "$$\n\\frac{a}{b}\n$$\n\nend";
|
|
63
|
+
assert.equal(normalizeDisplayMath(input), input);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("leaves content inside fenced code blocks untouched", () => {
|
|
67
|
+
const input = "```\n$$ not math $$\n$$\n```\n\nreal $$x = 1$$ end";
|
|
68
|
+
const normalized = normalizeDisplayMath(input);
|
|
69
|
+
assert.ok(normalized.includes("```\n$$ not math $$\n$$\n```"));
|
|
70
|
+
// every `$$` is preserved: 3 inside the fence + 2 in inline math
|
|
71
|
+
assert.equal(normalized.match(/\$\$/g)?.length, 5);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("leaves inline math and plain prose untouched", () => {
|
|
75
|
+
const input = "text $x = 1$ and $$a + b$$ more";
|
|
76
|
+
assert.equal(normalizeDisplayMath(input), input);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("does not treat a glued opener with mid-line $$ as a block", () => {
|
|
80
|
+
const input = "$$x$$ and text";
|
|
81
|
+
assert.equal(normalizeDisplayMath(input), input);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("\\[ … \\] blocks", () => {
|
|
86
|
+
it("normalizes single-line brackets", () => {
|
|
87
|
+
assert.equal(normalizeDisplayMath("\\[a + b\\]"), "$$\na + b\n$$");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("keeps content indented when nested in a list item", () => {
|
|
91
|
+
assert.equal(
|
|
92
|
+
normalizeDisplayMath("- item:\n \\[a + b\\]\n- next"),
|
|
93
|
+
"- item:\n $$\n a + b\n $$\n- next",
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("normalizes multi-line brackets without double-indenting", () => {
|
|
98
|
+
assert.equal(
|
|
99
|
+
normalizeDisplayMath("- item:\n \\[\n x = y\n \\]\n- next"),
|
|
100
|
+
"- item:\n $$\n x = y\n $$\n- next",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|