feat(runtime): improve provider delivery visibility
This commit is contained in:
parent
7138887a3b
commit
20c3194160
62 changed files with 2849 additions and 750 deletions
|
|
@ -93,17 +93,29 @@ const ruGuide: DefaultTheme.SidebarItem[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const rootNav: DefaultTheme.NavItem[] = [
|
const rootNav: DefaultTheme.NavItem[] = [
|
||||||
{ text: "Guide", link: "/guide/quickstart" },
|
{ text: "Guide", link: "/guide/quickstart", activeMatch: "^/guide/(?!troubleshooting(?:/|$))" },
|
||||||
{ text: "Reference", link: "/reference/concepts" },
|
{ text: "Reference", link: "/reference/concepts", activeMatch: "^/reference/" },
|
||||||
{ text: "Troubleshooting", link: "/guide/troubleshooting" },
|
{
|
||||||
{ text: "Download", link: downloadUrl, target: "_self" }
|
text: "Troubleshooting",
|
||||||
|
link: "/guide/troubleshooting",
|
||||||
|
activeMatch: "^/guide/troubleshooting(?:/|$)"
|
||||||
|
},
|
||||||
|
{ text: "Download", link: downloadUrl, target: "_self", noIcon: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
const ruNav: DefaultTheme.NavItem[] = [
|
const ruNav: DefaultTheme.NavItem[] = [
|
||||||
{ text: "Руководство", link: "/ru/guide/quickstart" },
|
{
|
||||||
{ text: "Справочник", link: "/ru/reference/concepts" },
|
text: "Руководство",
|
||||||
{ text: "Диагностика", link: "/ru/guide/troubleshooting" },
|
link: "/ru/guide/quickstart",
|
||||||
{ text: "Скачать", link: ruDownloadUrl, target: "_self" }
|
activeMatch: "^/ru/guide/(?!troubleshooting(?:/|$))"
|
||||||
|
},
|
||||||
|
{ text: "Справочник", link: "/ru/reference/concepts", activeMatch: "^/ru/reference/" },
|
||||||
|
{
|
||||||
|
text: "Диагностика",
|
||||||
|
link: "/ru/guide/troubleshooting",
|
||||||
|
activeMatch: "^/ru/guide/troubleshooting(?:/|$)"
|
||||||
|
},
|
||||||
|
{ text: "Скачать", link: ruDownloadUrl, target: "_self", noIcon: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|
@ -173,12 +185,20 @@ export default defineConfig({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
logo: "/logo-192.png",
|
logo: {
|
||||||
|
light: "/logo-192.png",
|
||||||
|
dark: "/logo-192.png",
|
||||||
|
alt: "Agent Teams"
|
||||||
|
},
|
||||||
siteTitle: "Agent Teams",
|
siteTitle: "Agent Teams",
|
||||||
outline: {
|
outline: {
|
||||||
level: [2, 3],
|
level: [2, 3],
|
||||||
label: "On this page"
|
label: "On this page"
|
||||||
},
|
},
|
||||||
|
externalLinkIcon: true,
|
||||||
|
darkModeSwitchLabel: "Appearance",
|
||||||
|
lightModeSwitchTitle: "Switch to light theme",
|
||||||
|
darkModeSwitchTitle: "Switch to dark theme",
|
||||||
search: {
|
search: {
|
||||||
provider: "local",
|
provider: "local",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -198,6 +218,14 @@ export default defineConfig({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
lastUpdated: {
|
||||||
|
text: "Last updated",
|
||||||
|
formatOptions: {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
forceLocale: true
|
||||||
|
}
|
||||||
|
},
|
||||||
nav: rootNav,
|
nav: rootNav,
|
||||||
sidebar: {
|
sidebar: {
|
||||||
"/ru/": ruGuide,
|
"/ru/": ruGuide,
|
||||||
|
|
@ -236,6 +264,9 @@ export default defineConfig({
|
||||||
level: [2, 3],
|
level: [2, 3],
|
||||||
label: "На этой странице"
|
label: "На этой странице"
|
||||||
},
|
},
|
||||||
|
darkModeSwitchLabel: "Оформление",
|
||||||
|
lightModeSwitchTitle: "Переключить на светлую тему",
|
||||||
|
darkModeSwitchTitle: "Переключить на тёмную тему",
|
||||||
search: {
|
search: {
|
||||||
provider: "local",
|
provider: "local",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -255,6 +286,14 @@ export default defineConfig({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
lastUpdated: {
|
||||||
|
text: "Обновлено",
|
||||||
|
formatOptions: {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
forceLocale: true
|
||||||
|
}
|
||||||
|
},
|
||||||
editLink: {
|
editLink: {
|
||||||
pattern: `https://github.com/${REPO}/edit/main/landing/product-docs/:path`,
|
pattern: `https://github.com/${REPO}/edit/main/landing/product-docs/:path`,
|
||||||
text: "Редактировать на GitHub"
|
text: "Редактировать на GitHub"
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -145,7 +145,7 @@
|
||||||
"cronstrue": "^3.13.0",
|
"cronstrue": "^3.13.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"diff": "^8.0.3",
|
"diff": "^8.0.3",
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.4.2",
|
||||||
"electron-updater": "^6.7.3",
|
"electron-updater": "^6.7.3",
|
||||||
"fastify": "^5.8.5",
|
"fastify": "^5.8.5",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
"isbinaryfile": "^6.0.0",
|
"isbinaryfile": "^6.0.0",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"mdast-util-to-hast": "^13.2.1",
|
"mdast-util-to-hast": "^13.2.1",
|
||||||
"mermaid": "^11.12.3",
|
"mermaid": "^11.15.0",
|
||||||
"motion": "12.38.0",
|
"motion": "12.38.0",
|
||||||
"node-diff3": "^3.2.0",
|
"node-diff3": "^3.2.0",
|
||||||
"node-pty": "^1.1.0",
|
"node-pty": "^1.1.0",
|
||||||
|
|
@ -177,7 +177,7 @@
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.9.0",
|
||||||
"yet-another-react-lightbox": "^3.29.1",
|
"yet-another-react-lightbox": "^3.29.1",
|
||||||
"zustand": "^4.5.0"
|
"zustand": "^4.5.0"
|
||||||
},
|
},
|
||||||
|
|
@ -330,6 +330,10 @@
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
|
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"lodash-es": "^4.18.1",
|
||||||
|
"uuid": "^11.1.1"
|
||||||
|
},
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"electron",
|
"electron",
|
||||||
"node-pty",
|
"node-pty",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,69 @@ export function truncateText(
|
||||||
return lo > 0 ? text.slice(0, lo) + '...' : '...';
|
return lo > 0 ? text.slice(0, lo) + '...' : '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fitTextPrefix(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
text: string,
|
||||||
|
maxWidth: number,
|
||||||
|
font: string,
|
||||||
|
): string {
|
||||||
|
let lo = 0;
|
||||||
|
let hi = text.length;
|
||||||
|
while (lo < hi) {
|
||||||
|
const mid = (lo + hi + 1) >> 1;
|
||||||
|
if (measureTextCached(ctx, font, text.slice(0, mid)) <= maxWidth) {
|
||||||
|
lo = mid;
|
||||||
|
} else {
|
||||||
|
hi = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text.slice(0, lo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap text into a small fixed number of canvas lines.
|
||||||
|
* The final line is truncated with "..." when the remaining text still overflows.
|
||||||
|
*/
|
||||||
|
export function wrapTextLines(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
text: string,
|
||||||
|
maxWidth: number,
|
||||||
|
font: string,
|
||||||
|
maxLines: number,
|
||||||
|
): string[] {
|
||||||
|
const normalized = text.trim().replace(/\s+/g, ' ');
|
||||||
|
if (!normalized || maxLines <= 0) return [];
|
||||||
|
if (maxLines === 1) return [truncateText(ctx, normalized, maxWidth, font)];
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
let remaining = normalized;
|
||||||
|
|
||||||
|
while (remaining && lines.length < maxLines) {
|
||||||
|
if (measureTextCached(ctx, font, remaining) <= maxWidth) {
|
||||||
|
lines.push(remaining);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.length === maxLines - 1) {
|
||||||
|
lines.push(truncateText(ctx, remaining, maxWidth, font));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = fitTextPrefix(ctx, remaining, maxWidth, font).trimEnd();
|
||||||
|
if (!prefix) {
|
||||||
|
lines.push(truncateText(ctx, remaining, maxWidth, font));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const breakIndex = prefix.lastIndexOf(' ');
|
||||||
|
const line = breakIndex > 0 ? prefix.slice(0, breakIndex) : prefix;
|
||||||
|
lines.push(line);
|
||||||
|
remaining = remaining.slice(line.length).trimStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-computed hex vertex unit offsets (avoids cos/sin per call)
|
// Pre-computed hex vertex unit offsets (avoids cos/sin per call)
|
||||||
const HEX_COS: number[] = [];
|
const HEX_COS: number[] = [];
|
||||||
const HEX_SIN: number[] = [];
|
const HEX_SIN: number[] = [];
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import type { GraphNode } from '../ports/types';
|
import type { GraphNode } from '../ports/types';
|
||||||
import { COLORS, getTaskStatusColor, getReviewStateColor } from '../constants/colors';
|
import { COLORS, getTaskStatusColor, getReviewStateColor } from '../constants/colors';
|
||||||
import { TASK_PILL, MIN_VISIBLE_OPACITY, ANIM } from '../constants/canvas-constants';
|
import { TASK_PILL, MIN_VISIBLE_OPACITY, ANIM } from '../constants/canvas-constants';
|
||||||
import { truncateText } from './draw-misc';
|
import { truncateText, wrapTextLines } from './draw-misc';
|
||||||
import { drawPillShell, drawPillStackLayer } from './draw-pill-shell';
|
import { drawPillShell, drawPillStackLayer } from './draw-pill-shell';
|
||||||
import { hexWithAlpha } from './render-cache';
|
import { hexWithAlpha } from './render-cache';
|
||||||
import type { KanbanZoneInfo } from '../layout/kanbanLayout';
|
import type { KanbanZoneInfo } from '../layout/kanbanLayout';
|
||||||
|
|
@ -153,7 +153,8 @@ function drawTaskPill(
|
||||||
drawLiveTaskLogIndicator(ctx, -halfW + 8, -halfH + 8, time);
|
drawLiveTaskLogIndicator(ctx, -halfW + 8, -halfH + 8, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subject (main title — large)
|
// Subject (main title - up to two lines)
|
||||||
|
let subjectLineCount = 0;
|
||||||
if (node.sublabel) {
|
if (node.sublabel) {
|
||||||
ctx.font = `bold ${TASK_PILL.idFontSize}px sans-serif`;
|
ctx.font = `bold ${TASK_PILL.idFontSize}px sans-serif`;
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
|
|
@ -164,8 +165,13 @@ function drawTaskPill(
|
||||||
node.reviewState !== 'approved' &&
|
node.reviewState !== 'approved' &&
|
||||||
(node.reviewMode === 'manual' || (node.reviewMode === 'assigned' && !!node.reviewerName));
|
(node.reviewMode === 'manual' || (node.reviewMode === 'assigned' && !!node.reviewerName));
|
||||||
const maxW = hasReviewChip ? w - 88 : w - 24;
|
const maxW = hasReviewChip ? w - 88 : w - 24;
|
||||||
const subject = truncateText(ctx, node.sublabel, maxW, ctx.font);
|
const subjectLines = wrapTextLines(ctx, node.sublabel, maxW, ctx.font, 2);
|
||||||
ctx.fillText(subject, textX, -12);
|
subjectLineCount = subjectLines.length;
|
||||||
|
const titleStartY = subjectLines.length > 1 ? -16 : -12;
|
||||||
|
const titleLineHeight = TASK_PILL.idFontSize + 1.5;
|
||||||
|
subjectLines.forEach((line, index) => {
|
||||||
|
ctx.fillText(line, textX, titleStartY + index * titleLineHeight);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display ID (secondary — small)
|
// Display ID (secondary — small)
|
||||||
|
|
@ -174,7 +180,7 @@ function drawTaskPill(
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.fillStyle = COLORS.textDim;
|
ctx.fillStyle = COLORS.textDim;
|
||||||
ctx.fillText(displayId, -halfW + 10, 12);
|
ctx.fillText(displayId, -halfW + 10, subjectLineCount > 1 ? 23 : 12);
|
||||||
|
|
||||||
// Approved badge: checkmark at right side
|
// Approved badge: checkmark at right side
|
||||||
if (node.reviewState === 'approved') {
|
if (node.reviewState === 'approved') {
|
||||||
|
|
|
||||||
342
pnpm-lock.yaml
342
pnpm-lock.yaml
|
|
@ -4,6 +4,10 @@ settings:
|
||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
lodash-es: ^4.18.1
|
||||||
|
uuid: ^11.1.1
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
|
@ -204,8 +208,8 @@ importers:
|
||||||
specifier: ^8.0.3
|
specifier: ^8.0.3
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
dompurify:
|
dompurify:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.4.2
|
||||||
version: 3.3.1
|
version: 3.4.2
|
||||||
electron-updater:
|
electron-updater:
|
||||||
specifier: ^6.7.3
|
specifier: ^6.7.3
|
||||||
version: 6.7.3
|
version: 6.7.3
|
||||||
|
|
@ -228,8 +232,8 @@ importers:
|
||||||
specifier: ^13.2.1
|
specifier: ^13.2.1
|
||||||
version: 13.2.1
|
version: 13.2.1
|
||||||
mermaid:
|
mermaid:
|
||||||
specifier: ^11.12.3
|
specifier: ^11.15.0
|
||||||
version: 11.12.3
|
version: 11.15.0
|
||||||
motion:
|
motion:
|
||||||
specifier: 12.38.0
|
specifier: 12.38.0
|
||||||
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
|
@ -295,13 +299,13 @@ importers:
|
||||||
version: 3.5.0
|
version: 3.5.0
|
||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))
|
version: 1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0))
|
||||||
unified:
|
unified:
|
||||||
specifier: ^11.0.5
|
specifier: ^11.0.5
|
||||||
version: 11.0.5
|
version: 11.0.5
|
||||||
yaml:
|
yaml:
|
||||||
specifier: ^2.8.2
|
specifier: ^2.9.0
|
||||||
version: 2.8.2
|
version: 2.9.0
|
||||||
yet-another-react-lightbox:
|
yet-another-react-lightbox:
|
||||||
specifier: ^3.29.1
|
specifier: ^3.29.1
|
||||||
version: 3.29.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 3.29.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
|
@ -323,7 +327,7 @@ importers:
|
||||||
version: 5.1.1(encoding@0.1.13)(rollup@4.60.0)
|
version: 5.1.1(encoding@0.1.13)(rollup@4.60.0)
|
||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.19
|
specifier: ^0.5.19
|
||||||
version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))
|
version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0))
|
||||||
'@types/hast':
|
'@types/hast':
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4
|
version: 3.0.4
|
||||||
|
|
@ -401,7 +405,7 @@ importers:
|
||||||
version: 3.0.6(eslint@9.39.2(jiti@1.21.7))
|
version: 3.0.6(eslint@9.39.2(jiti@1.21.7))
|
||||||
eslint-plugin-tailwindcss:
|
eslint-plugin-tailwindcss:
|
||||||
specifier: ^3.18.2
|
specifier: ^3.18.2
|
||||||
version: 3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))
|
version: 3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0))
|
||||||
globals:
|
globals:
|
||||||
specifier: ^17.2.0
|
specifier: ^17.2.0
|
||||||
version: 17.2.0
|
version: 17.2.0
|
||||||
|
|
@ -428,7 +432,7 @@ importers:
|
||||||
version: 0.7.2(prettier@3.8.1)
|
version: 0.7.2(prettier@3.8.1)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.4.1
|
specifier: ^3.4.1
|
||||||
version: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
version: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.21.0
|
specifier: ^4.21.0
|
||||||
version: 4.21.0
|
version: 4.21.0
|
||||||
|
|
@ -460,13 +464,13 @@ importers:
|
||||||
version: 0.11.3(magicast@0.5.2)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)))
|
version: 0.11.3(magicast@0.5.2)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)))
|
||||||
'@vueuse/nuxt':
|
'@vueuse/nuxt':
|
||||||
specifier: ^10.11.1
|
specifier: ^10.11.1
|
||||||
version: 10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
version: 10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^3.20.2
|
specifier: ^3.20.2
|
||||||
version: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
version: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)
|
||||||
nuxt-icon:
|
nuxt-icon:
|
||||||
specifier: ^0.6.10
|
specifier: ^0.6.10
|
||||||
version: 0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
version: 0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
|
version: 3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
|
||||||
|
|
@ -491,7 +495,7 @@ importers:
|
||||||
version: 7.4.47
|
version: 7.4.47
|
||||||
'@nuxt/eslint':
|
'@nuxt/eslint':
|
||||||
specifier: ^1.12.1
|
specifier: ^1.12.1
|
||||||
version: 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
version: 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||||
'@shikijs/transformers':
|
'@shikijs/transformers':
|
||||||
specifier: 3.22.0
|
specifier: 3.22.0
|
||||||
version: 3.22.0
|
version: 3.22.0
|
||||||
|
|
@ -512,13 +516,13 @@ importers:
|
||||||
version: 1.98.0
|
version: 1.98.0
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.4.19
|
specifier: ^3.4.19
|
||||||
version: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
version: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vite-plugin-vuetify:
|
vite-plugin-vuetify:
|
||||||
specifier: ^2.1.3
|
specifier: ^2.1.3
|
||||||
version: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
version: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||||
vitepress:
|
vitepress:
|
||||||
specifier: 2.0.0-alpha.17
|
specifier: 2.0.0-alpha.17
|
||||||
version: 2.0.0-alpha.17(@types/node@25.0.7)(axios@1.13.6)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(oxc-minify@0.117.0)(postcss@8.5.8)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
version: 2.0.0-alpha.17(@types/node@25.0.7)(axios@1.13.6)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(oxc-minify@0.117.0)(postcss@8.5.8)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)
|
||||||
vitepress-codeblock-collapse:
|
vitepress-codeblock-collapse:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0(vue@3.5.30(typescript@5.9.3))
|
version: 1.0.0(vue@3.5.30(typescript@5.9.3))
|
||||||
|
|
@ -543,7 +547,7 @@ importers:
|
||||||
version: 22.19.15
|
version: 22.19.15
|
||||||
tsup:
|
tsup:
|
||||||
specifier: ^8.5.1
|
specifier: ^8.5.1
|
||||||
version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.21.0
|
specifier: ^4.21.0
|
||||||
version: 4.21.0
|
version: 4.21.0
|
||||||
|
|
@ -784,21 +788,9 @@ packages:
|
||||||
'@braintree/sanitize-url@7.1.2':
|
'@braintree/sanitize-url@7.1.2':
|
||||||
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
|
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
|
||||||
|
|
||||||
'@chevrotain/cst-dts-gen@11.1.2':
|
|
||||||
resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==}
|
|
||||||
|
|
||||||
'@chevrotain/gast@11.1.2':
|
|
||||||
resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==}
|
|
||||||
|
|
||||||
'@chevrotain/regexp-to-ast@11.1.2':
|
|
||||||
resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==}
|
|
||||||
|
|
||||||
'@chevrotain/types@11.1.2':
|
'@chevrotain/types@11.1.2':
|
||||||
resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
|
resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
|
||||||
|
|
||||||
'@chevrotain/utils@11.1.2':
|
|
||||||
resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==}
|
|
||||||
|
|
||||||
'@clack/core@1.1.0':
|
'@clack/core@1.1.0':
|
||||||
resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==}
|
resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==}
|
||||||
|
|
||||||
|
|
@ -1979,8 +1971,8 @@ packages:
|
||||||
'@mdi/js@7.4.47':
|
'@mdi/js@7.4.47':
|
||||||
resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==}
|
resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==}
|
||||||
|
|
||||||
'@mermaid-js/parser@1.0.0':
|
'@mermaid-js/parser@1.1.1':
|
||||||
resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==}
|
resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==}
|
||||||
|
|
||||||
'@miyaneee/rollup-plugin-json5@1.2.0':
|
'@miyaneee/rollup-plugin-json5@1.2.0':
|
||||||
resolution: {integrity: sha512-JjTIaXZp9WzhUHpElrqPnl1AzBi/rvRs065F71+aTmlqvTMVkdbjZ8vfFl4nRlgJy+TPBw69ZK4pwFdmOAt4aA==}
|
resolution: {integrity: sha512-JjTIaXZp9WzhUHpElrqPnl1AzBi/rvRs065F71+aTmlqvTMVkdbjZ8vfFl4nRlgJy+TPBw69ZK4pwFdmOAt4aA==}
|
||||||
|
|
@ -4767,6 +4759,9 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@upsetjs/venn.js@2.0.0':
|
||||||
|
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
|
||||||
|
|
||||||
'@vercel/nft@1.5.0':
|
'@vercel/nft@1.5.0':
|
||||||
resolution: {integrity: sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==}
|
resolution: {integrity: sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
@ -5548,14 +5543,6 @@ packages:
|
||||||
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
|
|
||||||
chevrotain-allstar@0.3.1:
|
|
||||||
resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==}
|
|
||||||
peerDependencies:
|
|
||||||
chevrotain: ^11.0.0
|
|
||||||
|
|
||||||
chevrotain@11.1.2:
|
|
||||||
resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==}
|
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
@ -6031,8 +6018,8 @@ packages:
|
||||||
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
|
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
dagre-d3-es@7.0.13:
|
dagre-d3-es@7.0.14:
|
||||||
resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==}
|
resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==}
|
||||||
|
|
||||||
damerau-levenshtein@1.0.8:
|
damerau-levenshtein@1.0.8:
|
||||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||||
|
|
@ -6231,8 +6218,8 @@ packages:
|
||||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
dompurify@3.3.1:
|
dompurify@3.4.2:
|
||||||
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
|
resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==}
|
||||||
|
|
||||||
domutils@3.2.2:
|
domutils@3.2.2:
|
||||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||||
|
|
@ -6401,6 +6388,9 @@ packages:
|
||||||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-toolkit@1.46.1:
|
||||||
|
resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==}
|
||||||
|
|
||||||
es6-error@4.1.1:
|
es6-error@4.1.1:
|
||||||
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
|
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
|
||||||
|
|
||||||
|
|
@ -7828,10 +7818,6 @@ packages:
|
||||||
resolution: {integrity: sha512-2LOQnFKu3m0VxpE+5sb5+BRTSKrXmNxGgxVRiKwD9s5KQB1zID/FRXhtzeV7RT1L2GVpdEEAfVuclFOMGl1ikA==}
|
resolution: {integrity: sha512-2LOQnFKu3m0VxpE+5sb5+BRTSKrXmNxGgxVRiKwD9s5KQB1zID/FRXhtzeV7RT1L2GVpdEEAfVuclFOMGl1ikA==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
langium@4.2.1:
|
|
||||||
resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==}
|
|
||||||
engines: {node: '>=20.10.0', npm: '>=10.2.3'}
|
|
||||||
|
|
||||||
language-subtag-registry@0.3.23:
|
language-subtag-registry@0.3.23:
|
||||||
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
|
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
|
||||||
|
|
||||||
|
|
@ -7908,8 +7894,8 @@ packages:
|
||||||
resolution: {integrity: sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==}
|
resolution: {integrity: sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
lodash-es@4.17.23:
|
lodash-es@4.18.1:
|
||||||
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
|
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
|
||||||
|
|
||||||
lodash.defaults@4.2.0:
|
lodash.defaults@4.2.0:
|
||||||
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||||
|
|
@ -8122,8 +8108,8 @@ packages:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
mermaid@11.12.3:
|
mermaid@11.15.0:
|
||||||
resolution: {integrity: sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==}
|
resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==}
|
||||||
|
|
||||||
micromark-core-commonmark@2.0.3:
|
micromark-core-commonmark@2.0.3:
|
||||||
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
|
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
|
||||||
|
|
@ -10651,8 +10637,8 @@ packages:
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
uuid@11.1.0:
|
uuid@11.1.1:
|
||||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
|
|
@ -10876,23 +10862,6 @@ packages:
|
||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
vscode-jsonrpc@8.2.0:
|
|
||||||
resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
|
|
||||||
vscode-languageserver-protocol@3.17.5:
|
|
||||||
resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
|
|
||||||
|
|
||||||
vscode-languageserver-textdocument@1.0.12:
|
|
||||||
resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==}
|
|
||||||
|
|
||||||
vscode-languageserver-types@3.17.5:
|
|
||||||
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
|
|
||||||
|
|
||||||
vscode-languageserver@9.0.1:
|
|
||||||
resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
vscode-uri@3.1.0:
|
vscode-uri@3.1.0:
|
||||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||||
|
|
||||||
|
|
@ -11125,6 +11094,11 @@ packages:
|
||||||
engines: {node: '>= 14.6'}
|
engines: {node: '>= 14.6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
yaml@2.9.0:
|
||||||
|
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
||||||
|
engines: {node: '>= 14.6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
yargs-parser@21.1.1:
|
yargs-parser@21.1.1:
|
||||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -11504,23 +11478,8 @@ snapshots:
|
||||||
|
|
||||||
'@braintree/sanitize-url@7.1.2': {}
|
'@braintree/sanitize-url@7.1.2': {}
|
||||||
|
|
||||||
'@chevrotain/cst-dts-gen@11.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@chevrotain/gast': 11.1.2
|
|
||||||
'@chevrotain/types': 11.1.2
|
|
||||||
lodash-es: 4.17.23
|
|
||||||
|
|
||||||
'@chevrotain/gast@11.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@chevrotain/types': 11.1.2
|
|
||||||
lodash-es: 4.17.23
|
|
||||||
|
|
||||||
'@chevrotain/regexp-to-ast@11.1.2': {}
|
|
||||||
|
|
||||||
'@chevrotain/types@11.1.2': {}
|
'@chevrotain/types@11.1.2': {}
|
||||||
|
|
||||||
'@chevrotain/utils@11.1.2': {}
|
|
||||||
|
|
||||||
'@clack/core@1.1.0':
|
'@clack/core@1.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
sisteransi: 1.0.5
|
sisteransi: 1.0.5
|
||||||
|
|
@ -12745,9 +12704,9 @@ snapshots:
|
||||||
|
|
||||||
'@mdi/js@7.4.47': {}
|
'@mdi/js@7.4.47': {}
|
||||||
|
|
||||||
'@mermaid-js/parser@1.0.0':
|
'@mermaid-js/parser@1.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
langium: 4.2.1
|
'@chevrotain/types': 11.1.2
|
||||||
|
|
||||||
'@miyaneee/rollup-plugin-json5@1.2.0(rollup@4.60.0)':
|
'@miyaneee/rollup-plugin-json5@1.2.0(rollup@4.60.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -12857,20 +12816,20 @@ snapshots:
|
||||||
|
|
||||||
'@nuxt/devalue@2.0.2': {}
|
'@nuxt/devalue@2.0.2': {}
|
||||||
|
|
||||||
'@nuxt/devtools-kit@1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
'@nuxt/devtools-kit@1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||||
'@nuxt/schema': 3.21.2
|
'@nuxt/schema': 3.21.2
|
||||||
execa: 7.2.0
|
execa: 7.2.0
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
'@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
'@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||||
execa: 8.0.1
|
execa: 8.0.1
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
|
|
@ -12885,9 +12844,9 @@ snapshots:
|
||||||
pkg-types: 2.3.0
|
pkg-types: 2.3.0
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
|
|
||||||
'@nuxt/devtools@3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
'@nuxt/devtools@3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||||
'@nuxt/devtools-wizard': 3.2.4
|
'@nuxt/devtools-wizard': 3.2.4
|
||||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||||
'@vue/devtools-core': 8.1.0(vue@3.5.30(typescript@5.9.3))
|
'@vue/devtools-core': 8.1.0(vue@3.5.30(typescript@5.9.3))
|
||||||
|
|
@ -12915,9 +12874,9 @@ snapshots:
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
structured-clone-es: 2.0.0
|
structured-clone-es: 2.0.0
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||||
vite-plugin-vue-tracer: 1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
vite-plugin-vue-tracer: 1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||||
which: 6.0.1
|
which: 6.0.1
|
||||||
ws: 8.20.0
|
ws: 8.20.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|
@ -12966,10 +12925,10 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
'@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/config-inspector': 1.5.0(eslint@9.39.2(jiti@2.6.1))
|
'@eslint/config-inspector': 1.5.0(eslint@9.39.2(jiti@2.6.1))
|
||||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||||
'@nuxt/eslint-config': 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@nuxt/eslint-config': 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@nuxt/eslint-plugin': 1.15.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@nuxt/eslint-plugin': 1.15.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||||
|
|
@ -13045,7 +13004,7 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
'@nuxt/nitro-server@3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)':
|
'@nuxt/nitro-server@3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/devalue': 2.0.2
|
'@nuxt/devalue': 2.0.2
|
||||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||||
|
|
@ -13063,7 +13022,7 @@ snapshots:
|
||||||
klona: 2.0.6
|
klona: 2.0.6
|
||||||
mocked-exports: 0.1.1
|
mocked-exports: 0.1.1
|
||||||
nitropack: 2.13.2(encoding@0.1.13)(idb-keyval@6.2.2)
|
nitropack: 2.13.2(encoding@0.1.13)(idb-keyval@6.2.2)
|
||||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
pkg-types: 2.3.0
|
pkg-types: 2.3.0
|
||||||
|
|
@ -13128,12 +13087,12 @@ snapshots:
|
||||||
rc9: 3.0.0
|
rc9: 3.0.0
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
|
|
||||||
'@nuxt/vite-builder@3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)':
|
'@nuxt/vite-builder@3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||||
'@rollup/plugin-replace': 6.0.3(rollup@4.60.0)
|
'@rollup/plugin-replace': 6.0.3(rollup@4.60.0)
|
||||||
'@vitejs/plugin-vue': 6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
'@vitejs/plugin-vue': 6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||||
'@vitejs/plugin-vue-jsx': 5.1.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
'@vitejs/plugin-vue-jsx': 5.1.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||||
autoprefixer: 10.5.0(postcss@8.5.8)
|
autoprefixer: 10.5.0(postcss@8.5.8)
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
cssnano: 7.1.3(postcss@8.5.8)
|
cssnano: 7.1.3(postcss@8.5.8)
|
||||||
|
|
@ -13147,7 +13106,7 @@ snapshots:
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.2
|
mlly: 1.8.2
|
||||||
mocked-exports: 0.1.1
|
mocked-exports: 0.1.1
|
||||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)
|
||||||
nypm: 0.6.5
|
nypm: 0.6.5
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
@ -13158,9 +13117,9 @@ snapshots:
|
||||||
std-env: 4.0.0
|
std-env: 4.0.0
|
||||||
ufo: 1.6.3
|
ufo: 1.6.3
|
||||||
unenv: 2.0.0-rc.24
|
unenv: 2.0.0-rc.24
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vite-node: 5.3.0(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite-node: 5.3.0(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vite-plugin-checker: 0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
vite-plugin-checker: 0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||||
vue: 3.5.30(typescript@5.9.3)
|
vue: 3.5.30(typescript@5.9.3)
|
||||||
vue-bundle-renderer: 2.2.0
|
vue-bundle-renderer: 2.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|
@ -14875,10 +14834,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
defer-to-connect: 2.0.1
|
defer-to-connect: 2.0.1
|
||||||
|
|
||||||
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))':
|
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss-selector-parser: 6.0.10
|
postcss-selector-parser: 6.0.10
|
||||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||||
|
|
||||||
'@tanstack/react-virtual@3.13.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@tanstack/react-virtual@3.13.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -15638,6 +15597,11 @@ snapshots:
|
||||||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@upsetjs/venn.js@2.0.0':
|
||||||
|
optionalDependencies:
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||||
|
|
||||||
'@vercel/nft@1.5.0(encoding@0.1.13)(rollup@4.60.0)':
|
'@vercel/nft@1.5.0(encoding@0.1.13)(rollup@4.60.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13)
|
'@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13)
|
||||||
|
|
@ -15669,22 +15633,22 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
'@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
|
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
|
||||||
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
|
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.11
|
'@rolldown/pluginutils': 1.0.0-rc.11
|
||||||
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
|
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vue: 3.5.30(typescript@5.9.3)
|
vue: 3.5.30(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
'@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.2
|
'@rolldown/pluginutils': 1.0.0-rc.2
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vue: 3.5.30(typescript@5.9.3)
|
vue: 3.5.30(typescript@5.9.3)
|
||||||
|
|
||||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.0.2)(sass@1.98.0)(terser@5.46.0))':
|
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.0.2)(sass@1.98.0)(terser@5.46.0))':
|
||||||
|
|
@ -15955,13 +15919,13 @@ snapshots:
|
||||||
|
|
||||||
'@vueuse/metadata@14.3.0': {}
|
'@vueuse/metadata@14.3.0': {}
|
||||||
|
|
||||||
'@vueuse/nuxt@10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
'@vueuse/nuxt@10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||||
'@vueuse/core': 10.11.1(vue@3.5.30(typescript@5.9.3))
|
'@vueuse/core': 10.11.1(vue@3.5.30(typescript@5.9.3))
|
||||||
'@vueuse/metadata': 10.11.1
|
'@vueuse/metadata': 10.11.1
|
||||||
local-pkg: 0.5.1
|
local-pkg: 0.5.1
|
||||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)
|
||||||
vue-demi: 0.14.10(vue@3.5.30(typescript@5.9.3))
|
vue-demi: 0.14.10(vue@3.5.30(typescript@5.9.3))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
|
|
@ -16614,20 +16578,6 @@ snapshots:
|
||||||
|
|
||||||
check-error@2.1.3: {}
|
check-error@2.1.3: {}
|
||||||
|
|
||||||
chevrotain-allstar@0.3.1(chevrotain@11.1.2):
|
|
||||||
dependencies:
|
|
||||||
chevrotain: 11.1.2
|
|
||||||
lodash-es: 4.17.23
|
|
||||||
|
|
||||||
chevrotain@11.1.2:
|
|
||||||
dependencies:
|
|
||||||
'@chevrotain/cst-dts-gen': 11.1.2
|
|
||||||
'@chevrotain/gast': 11.1.2
|
|
||||||
'@chevrotain/regexp-to-ast': 11.1.2
|
|
||||||
'@chevrotain/types': 11.1.2
|
|
||||||
'@chevrotain/utils': 11.1.2
|
|
||||||
lodash-es: 4.17.23
|
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
anymatch: 3.1.3
|
anymatch: 3.1.3
|
||||||
|
|
@ -17129,10 +17079,10 @@ snapshots:
|
||||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||||
d3-zoom: 3.0.0
|
d3-zoom: 3.0.0
|
||||||
|
|
||||||
dagre-d3-es@7.0.13:
|
dagre-d3-es@7.0.14:
|
||||||
dependencies:
|
dependencies:
|
||||||
d3: 7.9.0
|
d3: 7.9.0
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.18.1
|
||||||
|
|
||||||
damerau-levenshtein@1.0.8: {}
|
damerau-levenshtein@1.0.8: {}
|
||||||
|
|
||||||
|
|
@ -17298,7 +17248,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
domelementtype: 2.3.0
|
domelementtype: 2.3.0
|
||||||
|
|
||||||
dompurify@3.3.1:
|
dompurify@3.4.2:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/trusted-types': 2.0.7
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
|
|
@ -17566,6 +17516,8 @@ snapshots:
|
||||||
is-date-object: 1.1.0
|
is-date-object: 1.1.0
|
||||||
is-symbol: 1.1.1
|
is-symbol: 1.1.1
|
||||||
|
|
||||||
|
es-toolkit@1.46.1: {}
|
||||||
|
|
||||||
es6-error@4.1.1:
|
es6-error@4.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -17955,11 +17907,11 @@ snapshots:
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)):
|
eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||||
|
|
||||||
eslint-plugin-unicorn@63.0.0(eslint@9.39.2(jiti@2.6.1)):
|
eslint-plugin-unicorn@63.0.0(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -19447,14 +19399,6 @@ snapshots:
|
||||||
type-is: 2.0.1
|
type-is: 2.0.1
|
||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
|
|
||||||
langium@4.2.1:
|
|
||||||
dependencies:
|
|
||||||
chevrotain: 11.1.2
|
|
||||||
chevrotain-allstar: 0.3.1(chevrotain@11.1.2)
|
|
||||||
vscode-languageserver: 9.0.1
|
|
||||||
vscode-languageserver-textdocument: 1.0.12
|
|
||||||
vscode-uri: 3.1.0
|
|
||||||
|
|
||||||
language-subtag-registry@0.3.23: {}
|
language-subtag-registry@0.3.23: {}
|
||||||
|
|
||||||
language-tags@1.0.9:
|
language-tags@1.0.9:
|
||||||
|
|
@ -19505,7 +19449,7 @@ snapshots:
|
||||||
nano-spawn: 2.0.0
|
nano-spawn: 2.0.0
|
||||||
pidtree: 0.6.0
|
pidtree: 0.6.0
|
||||||
string-argv: 0.3.2
|
string-argv: 0.3.2
|
||||||
yaml: 2.8.2
|
yaml: 2.9.0
|
||||||
|
|
||||||
listhen@1.9.0:
|
listhen@1.9.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -19558,7 +19502,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 6.0.0
|
p-locate: 6.0.0
|
||||||
|
|
||||||
lodash-es@4.17.23: {}
|
lodash-es@4.18.1: {}
|
||||||
|
|
||||||
lodash.defaults@4.2.0: {}
|
lodash.defaults@4.2.0: {}
|
||||||
|
|
||||||
|
|
@ -19904,28 +19848,29 @@ snapshots:
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
mermaid@11.12.3:
|
mermaid@11.15.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@braintree/sanitize-url': 7.1.2
|
'@braintree/sanitize-url': 7.1.2
|
||||||
'@iconify/utils': 3.1.0
|
'@iconify/utils': 3.1.0
|
||||||
'@mermaid-js/parser': 1.0.0
|
'@mermaid-js/parser': 1.1.1
|
||||||
'@types/d3': 7.4.3
|
'@types/d3': 7.4.3
|
||||||
|
'@upsetjs/venn.js': 2.0.0
|
||||||
cytoscape: 3.33.1
|
cytoscape: 3.33.1
|
||||||
cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1)
|
cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1)
|
||||||
cytoscape-fcose: 2.2.0(cytoscape@3.33.1)
|
cytoscape-fcose: 2.2.0(cytoscape@3.33.1)
|
||||||
d3: 7.9.0
|
d3: 7.9.0
|
||||||
d3-sankey: 0.12.3
|
d3-sankey: 0.12.3
|
||||||
dagre-d3-es: 7.0.13
|
dagre-d3-es: 7.0.14
|
||||||
dayjs: 1.11.19
|
dayjs: 1.11.19
|
||||||
dompurify: 3.3.1
|
dompurify: 3.4.2
|
||||||
|
es-toolkit: 1.46.1
|
||||||
katex: 0.16.33
|
katex: 0.16.33
|
||||||
khroma: 2.1.0
|
khroma: 2.1.0
|
||||||
lodash-es: 4.17.23
|
|
||||||
marked: 16.4.2
|
marked: 16.4.2
|
||||||
roughjs: 4.6.6
|
roughjs: 4.6.6
|
||||||
stylis: 4.3.6
|
stylis: 4.3.6
|
||||||
ts-dedent: 2.2.0
|
ts-dedent: 2.2.0
|
||||||
uuid: 11.1.0
|
uuid: 11.1.1
|
||||||
|
|
||||||
micromark-core-commonmark@2.0.3:
|
micromark-core-commonmark@2.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -20467,27 +20412,27 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
|
|
||||||
nuxt-icon@0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)):
|
nuxt-icon@0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/collections': 1.0.665
|
'@iconify/collections': 1.0.665
|
||||||
'@iconify/vue': 4.3.0(vue@3.5.30(typescript@5.9.3))
|
'@iconify/vue': 4.3.0(vue@3.5.30(typescript@5.9.3))
|
||||||
'@nuxt/devtools-kit': 1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
'@nuxt/devtools-kit': 1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
- vite
|
- vite
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2):
|
nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@dxup/nuxt': 0.4.0(magicast@0.5.2)(typescript@5.9.3)
|
'@dxup/nuxt': 0.4.0(magicast@0.5.2)(typescript@5.9.3)
|
||||||
'@nuxt/cli': 3.34.0(@nuxt/schema@3.21.2)(cac@6.7.14)(magicast@0.5.2)
|
'@nuxt/cli': 3.34.0(@nuxt/schema@3.21.2)(cac@6.7.14)(magicast@0.5.2)
|
||||||
'@nuxt/devtools': 3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
'@nuxt/devtools': 3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||||
'@nuxt/nitro-server': 3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)
|
'@nuxt/nitro-server': 3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(typescript@5.9.3)
|
||||||
'@nuxt/schema': 3.21.2
|
'@nuxt/schema': 3.21.2
|
||||||
'@nuxt/telemetry': 2.7.0(@nuxt/kit@3.21.2(magicast@0.5.2))
|
'@nuxt/telemetry': 2.7.0(@nuxt/kit@3.21.2(magicast@0.5.2))
|
||||||
'@nuxt/vite-builder': 3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)
|
'@nuxt/vite-builder': 3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.9.0)
|
||||||
'@unhead/vue': 2.1.12(vue@3.5.30(typescript@5.9.3))
|
'@unhead/vue': 2.1.12(vue@3.5.30(typescript@5.9.3))
|
||||||
'@vue/shared': 3.5.30
|
'@vue/shared': 3.5.30
|
||||||
c12: 3.3.3(magicast@0.5.2)
|
c12: 3.3.3(magicast@0.5.2)
|
||||||
|
|
@ -21111,23 +21056,23 @@ snapshots:
|
||||||
camelcase-css: 2.0.1
|
camelcase-css: 2.0.1
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
|
|
||||||
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2):
|
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
jiti: 1.21.7
|
jiti: 1.21.7
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
yaml: 2.8.2
|
yaml: 2.9.0
|
||||||
|
|
||||||
postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2):
|
postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
postcss: 8.5.8
|
postcss: 8.5.8
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
yaml: 2.8.2
|
yaml: 2.9.0
|
||||||
|
|
||||||
postcss-merge-longhand@7.0.5(postcss@8.5.8):
|
postcss-merge-longhand@7.0.5(postcss@8.5.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -22461,11 +22406,11 @@ snapshots:
|
||||||
|
|
||||||
tailwind-merge@3.5.0: {}
|
tailwind-merge@3.5.0: {}
|
||||||
|
|
||||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)):
|
tailwindcss-animate@1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||||
|
|
||||||
tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2):
|
tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
|
|
@ -22484,7 +22429,7 @@ snapshots:
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-import: 15.1.0(postcss@8.5.6)
|
postcss-import: 15.1.0(postcss@8.5.6)
|
||||||
postcss-js: 4.1.0(postcss@8.5.6)
|
postcss-js: 4.1.0(postcss@8.5.6)
|
||||||
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2)
|
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
postcss-nested: 6.2.0(postcss@8.5.6)
|
postcss-nested: 6.2.0(postcss@8.5.6)
|
||||||
postcss-selector-parser: 6.1.2
|
postcss-selector-parser: 6.1.2
|
||||||
resolve: 1.22.11
|
resolve: 1.22.11
|
||||||
|
|
@ -22655,7 +22600,7 @@ snapshots:
|
||||||
|
|
||||||
tsscmp@1.0.6: {}
|
tsscmp@1.0.6: {}
|
||||||
|
|
||||||
tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2):
|
tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
bundle-require: 5.1.0(esbuild@0.27.2)
|
bundle-require: 5.1.0(esbuild@0.27.2)
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
|
|
@ -22666,7 +22611,7 @@ snapshots:
|
||||||
fix-dts-default-cjs-exports: 1.0.1
|
fix-dts-default-cjs-exports: 1.0.1
|
||||||
joycon: 3.1.1
|
joycon: 3.1.1
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2)
|
postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
rollup: 4.55.1
|
rollup: 4.55.1
|
||||||
source-map: 0.7.6
|
source-map: 0.7.6
|
||||||
|
|
@ -23084,7 +23029,7 @@ snapshots:
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
uuid@11.1.0: {}
|
uuid@11.1.1: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
|
|
@ -23110,15 +23055,15 @@ snapshots:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
vite-dev-rpc@1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
vite-dev-rpc@1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
birpc: 2.9.0
|
birpc: 2.9.0
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vite-hot-client: 2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
vite-hot-client: 2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||||
|
|
||||||
vite-hot-client@2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
vite-hot-client@2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
|
|
||||||
vite-node@3.2.4(@types/node@22.19.15)(sass@1.98.0)(terser@5.46.0):
|
vite-node@3.2.4(@types/node@22.19.15)(sass@1.98.0)(terser@5.46.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -23156,13 +23101,13 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vite-node@5.3.0(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
vite-node@5.3.0(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
es-module-lexer: 2.0.0
|
es-module-lexer: 2.0.0
|
||||||
obug: 2.1.1
|
obug: 2.1.1
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
|
|
@ -23176,7 +23121,7 @@ snapshots:
|
||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
vite-plugin-checker@0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
vite-plugin-checker@0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.0
|
||||||
chokidar: 4.0.3
|
chokidar: 4.0.3
|
||||||
|
|
@ -23185,14 +23130,14 @@ snapshots:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vscode-uri: 3.1.0
|
vscode-uri: 3.1.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
optionator: 0.9.4
|
optionator: 0.9.4
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
ansis: 4.2.0
|
ansis: 4.2.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
|
|
@ -23202,29 +23147,29 @@ snapshots:
|
||||||
perfect-debounce: 2.1.0
|
perfect-debounce: 2.1.0
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
unplugin-utils: 0.3.1
|
unplugin-utils: 0.3.1
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vite-dev-rpc: 1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
vite-dev-rpc: 1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vite-plugin-vue-tracer@1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)):
|
vite-plugin-vue-tracer@1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
exsolve: 1.0.8
|
exsolve: 1.0.8
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vue: 3.5.30(typescript@5.9.3)
|
vue: 3.5.30(typescript@5.9.3)
|
||||||
|
|
||||||
vite-plugin-vuetify@2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3):
|
vite-plugin-vuetify@2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vuetify/loader-shared': 2.1.2(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
'@vuetify/loader-shared': 2.1.2(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
upath: 2.0.1
|
upath: 2.0.1
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vue: 3.5.30(typescript@5.9.3)
|
vue: 3.5.30(typescript@5.9.3)
|
||||||
vuetify: 3.12.3(typescript@5.9.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.30(typescript@5.9.3))
|
vuetify: 3.12.3(typescript@5.9.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.30(typescript@5.9.3))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|
@ -23252,7 +23197,7 @@ snapshots:
|
||||||
sass: 1.98.0
|
sass: 1.98.0
|
||||||
terser: 5.46.0
|
terser: 5.46.0
|
||||||
|
|
||||||
vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.4
|
esbuild: 0.27.4
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
|
|
@ -23267,7 +23212,7 @@ snapshots:
|
||||||
sass: 1.98.0
|
sass: 1.98.0
|
||||||
terser: 5.46.0
|
terser: 5.46.0
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
yaml: 2.8.2
|
yaml: 2.9.0
|
||||||
|
|
||||||
vitepress-codeblock-collapse@1.0.0(vue@3.5.30(typescript@5.9.3)):
|
vitepress-codeblock-collapse@1.0.0(vue@3.5.30(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -23292,7 +23237,7 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vitepress@2.0.0-alpha.17(@types/node@25.0.7)(axios@1.13.6)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(oxc-minify@0.117.0)(postcss@8.5.8)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2):
|
vitepress@2.0.0-alpha.17(@types/node@25.0.7)(axios@1.13.6)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(oxc-minify@0.117.0)(postcss@8.5.8)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@docsearch/css': 4.6.3
|
'@docsearch/css': 4.6.3
|
||||||
'@docsearch/js': 4.6.3
|
'@docsearch/js': 4.6.3
|
||||||
|
|
@ -23302,7 +23247,7 @@ snapshots:
|
||||||
'@shikijs/transformers': 3.22.0
|
'@shikijs/transformers': 3.22.0
|
||||||
'@shikijs/types': 3.23.0
|
'@shikijs/types': 3.23.0
|
||||||
'@types/markdown-it': 14.1.2
|
'@types/markdown-it': 14.1.2
|
||||||
'@vitejs/plugin-vue': 6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
'@vitejs/plugin-vue': 6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||||
'@vue/devtools-api': 8.1.1
|
'@vue/devtools-api': 8.1.1
|
||||||
'@vue/shared': 3.5.30
|
'@vue/shared': 3.5.30
|
||||||
'@vueuse/core': 14.3.0(vue@3.5.30(typescript@5.9.3))
|
'@vueuse/core': 14.3.0(vue@3.5.30(typescript@5.9.3))
|
||||||
|
|
@ -23311,7 +23256,7 @@ snapshots:
|
||||||
mark.js: 8.11.1
|
mark.js: 8.11.1
|
||||||
minisearch: 7.2.0
|
minisearch: 7.2.0
|
||||||
shiki: 3.23.0
|
shiki: 3.23.0
|
||||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
vue: 3.5.30(typescript@5.9.3)
|
vue: 3.5.30(typescript@5.9.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
oxc-minify: 0.117.0
|
oxc-minify: 0.117.0
|
||||||
|
|
@ -23421,21 +23366,6 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vscode-jsonrpc@8.2.0: {}
|
|
||||||
|
|
||||||
vscode-languageserver-protocol@3.17.5:
|
|
||||||
dependencies:
|
|
||||||
vscode-jsonrpc: 8.2.0
|
|
||||||
vscode-languageserver-types: 3.17.5
|
|
||||||
|
|
||||||
vscode-languageserver-textdocument@1.0.12: {}
|
|
||||||
|
|
||||||
vscode-languageserver-types@3.17.5: {}
|
|
||||||
|
|
||||||
vscode-languageserver@9.0.1:
|
|
||||||
dependencies:
|
|
||||||
vscode-languageserver-protocol: 3.17.5
|
|
||||||
|
|
||||||
vscode-uri@3.1.0: {}
|
vscode-uri@3.1.0: {}
|
||||||
|
|
||||||
vue-bundle-renderer@2.2.0:
|
vue-bundle-renderer@2.2.0:
|
||||||
|
|
@ -23495,7 +23425,7 @@ snapshots:
|
||||||
vue: 3.5.30(typescript@5.9.3)
|
vue: 3.5.30(typescript@5.9.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
vite-plugin-vuetify: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
vite-plugin-vuetify: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||||
|
|
||||||
w3c-keyname@2.2.8: {}
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
|
|
@ -23639,6 +23569,8 @@ snapshots:
|
||||||
|
|
||||||
yaml@2.8.2: {}
|
yaml@2.8.2: {}
|
||||||
|
|
||||||
|
yaml@2.9.0: {}
|
||||||
|
|
||||||
yargs-parser@21.1.1: {}
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
yargs-parser@22.0.0: {}
|
yargs-parser@22.0.0: {}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
{
|
{
|
||||||
"version": "0.0.30",
|
"version": "0.0.31",
|
||||||
"sourceRef": "v0.0.30",
|
"sourceRef": "v0.0.31",
|
||||||
"sourceRepository": "777genius/agent_teams_orchestrator",
|
"sourceRepository": "777genius/agent_teams_orchestrator",
|
||||||
"releaseRepository": "777genius/agent-teams-ai",
|
"releaseRepository": "777genius/agent-teams-ai",
|
||||||
"releaseTag": "v1.2.0",
|
"releaseTag": "v1.2.0",
|
||||||
"assets": {
|
"assets": {
|
||||||
"darwin-arm64": {
|
"darwin-arm64": {
|
||||||
"file": "agent-teams-runtime-darwin-arm64-v0.0.30.tar.gz",
|
"file": "agent-teams-runtime-darwin-arm64-v0.0.31.tar.gz",
|
||||||
"archiveKind": "tar.gz",
|
"archiveKind": "tar.gz",
|
||||||
"binaryName": "claude-multimodel"
|
"binaryName": "claude-multimodel"
|
||||||
},
|
},
|
||||||
"darwin-x64": {
|
"darwin-x64": {
|
||||||
"file": "agent-teams-runtime-darwin-x64-v0.0.30.tar.gz",
|
"file": "agent-teams-runtime-darwin-x64-v0.0.31.tar.gz",
|
||||||
"archiveKind": "tar.gz",
|
"archiveKind": "tar.gz",
|
||||||
"binaryName": "claude-multimodel"
|
"binaryName": "claude-multimodel"
|
||||||
},
|
},
|
||||||
"linux-x64": {
|
"linux-x64": {
|
||||||
"file": "agent-teams-runtime-linux-x64-v0.0.30.tar.gz",
|
"file": "agent-teams-runtime-linux-x64-v0.0.31.tar.gz",
|
||||||
"archiveKind": "tar.gz",
|
"archiveKind": "tar.gz",
|
||||||
"binaryName": "claude-multimodel"
|
"binaryName": "claude-multimodel"
|
||||||
},
|
},
|
||||||
"win32-x64": {
|
"win32-x64": {
|
||||||
"file": "agent-teams-runtime-win32-x64-v0.0.30.zip",
|
"file": "agent-teams-runtime-win32-x64-v0.0.31.zip",
|
||||||
"archiveKind": "zip",
|
"archiveKind": "zip",
|
||||||
"binaryName": "claude-multimodel.exe"
|
"binaryName": "claude-multimodel.exe"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -770,8 +770,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadApiKeyAvailability(): Promise<CodexApiKeyAvailabilityDto> {
|
private async loadApiKeyAvailability(): Promise<CodexApiKeyAvailabilityDto> {
|
||||||
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
if (await this.apiKeyService.hasPreferred('OPENAI_API_KEY')) {
|
||||||
if (storedKey?.value.trim()) {
|
|
||||||
return {
|
return {
|
||||||
available: true,
|
available: true,
|
||||||
source: 'stored',
|
source: 'stored',
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export class ClaudeMemberTranscriptPreviewSource implements MemberLogPreviewSour
|
||||||
[input.memberName],
|
[input.memberName],
|
||||||
{
|
{
|
||||||
forceRefresh: input.forceRefresh === true,
|
forceRefresh: input.forceRefresh === true,
|
||||||
|
includeTeamSubagentSessionDiscovery: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const dedupedRefs = dedupeMemberLogRefs(refs);
|
const dedupedRefs = dedupeMemberLogRefs(refs);
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,6 @@ import {
|
||||||
PluginCatalogService,
|
PluginCatalogService,
|
||||||
PluginInstallationStateService,
|
PluginInstallationStateService,
|
||||||
PluginInstallService,
|
PluginInstallService,
|
||||||
RUNTIME_MANAGED_API_KEY_ENV_VARS,
|
|
||||||
SkillsCatalogService,
|
SkillsCatalogService,
|
||||||
SkillsMutationService,
|
SkillsMutationService,
|
||||||
SkillsWatcherService,
|
SkillsWatcherService,
|
||||||
|
|
@ -1478,7 +1477,6 @@ async function initializeServices(): Promise<void> {
|
||||||
phase: 'settings',
|
phase: 'settings',
|
||||||
message: 'Loading secure settings...',
|
message: 'Loading secure settings...',
|
||||||
});
|
});
|
||||||
await apiKeyService.syncProcessEnv(RUNTIME_MANAGED_API_KEY_ENV_VARS);
|
|
||||||
// warmup() and ensureInstalled() are deferred to after window creation
|
// warmup() and ensureInstalled() are deferred to after window creation
|
||||||
// (did-finish-load handler) to avoid thread pool contention at startup.
|
// (did-finish-load handler) to avoid thread pool contention at startup.
|
||||||
httpServer = new HttpServer();
|
httpServer = new HttpServer();
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,7 @@ import {
|
||||||
} from '@preload/constants/ipcChannels';
|
} from '@preload/constants/ipcChannels';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
|
||||||
import {
|
import { type ApiKeyService } from '../services/extensions/apikeys/ApiKeyService';
|
||||||
type ApiKeyService,
|
|
||||||
RUNTIME_MANAGED_API_KEY_ENV_VARS,
|
|
||||||
} from '../services/extensions/apikeys/ApiKeyService';
|
|
||||||
import { GitHubStarsService } from '../services/extensions/catalog/GitHubStarsService';
|
import { GitHubStarsService } from '../services/extensions/catalog/GitHubStarsService';
|
||||||
|
|
||||||
import type { ExtensionFacadeService } from '../services/extensions/ExtensionFacadeService';
|
import type { ExtensionFacadeService } from '../services/extensions/ExtensionFacadeService';
|
||||||
|
|
@ -396,12 +393,7 @@ async function handleApiKeysSave(
|
||||||
): Promise<IpcResult<ApiKeyEntry>> {
|
): Promise<IpcResult<ApiKeyEntry>> {
|
||||||
return wrapHandler('apiKeysSave', () => {
|
return wrapHandler('apiKeysSave', () => {
|
||||||
if (!request) throw new Error('Request is required');
|
if (!request) throw new Error('Request is required');
|
||||||
return getApiKeyService()
|
return getApiKeyService().save(request);
|
||||||
.save(request)
|
|
||||||
.then(async (entry) => {
|
|
||||||
await getApiKeyService().syncProcessEnv(RUNTIME_MANAGED_API_KEY_ENV_VARS);
|
|
||||||
return entry;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,11 +403,7 @@ async function handleApiKeysDelete(
|
||||||
): Promise<IpcResult<void>> {
|
): Promise<IpcResult<void>> {
|
||||||
return wrapHandler('apiKeysDelete', () => {
|
return wrapHandler('apiKeysDelete', () => {
|
||||||
if (typeof id !== 'string' || !id) throw new Error('Key ID is required');
|
if (typeof id !== 'string' || !id) throw new Error('Key ID is required');
|
||||||
return getApiKeyService()
|
return getApiKeyService().delete(id);
|
||||||
.delete(id)
|
|
||||||
.then(async () => {
|
|
||||||
await getApiKeyService().syncProcessEnv(RUNTIME_MANAGED_API_KEY_ENV_VARS);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,9 +215,9 @@ import type {
|
||||||
TeamCreateResponse,
|
TeamCreateResponse,
|
||||||
TeamFastMode,
|
TeamFastMode,
|
||||||
TeamGetDataOptions,
|
TeamGetDataOptions,
|
||||||
|
TeamLaunchFailureDiagnosticsBundle,
|
||||||
TeamLaunchRequest,
|
TeamLaunchRequest,
|
||||||
TeamLaunchResponse,
|
TeamLaunchResponse,
|
||||||
TeamLaunchFailureDiagnosticsBundle,
|
|
||||||
TeamMemberActivityMeta,
|
TeamMemberActivityMeta,
|
||||||
TeamMessageNotificationData,
|
TeamMessageNotificationData,
|
||||||
TeamProviderBackendId,
|
TeamProviderBackendId,
|
||||||
|
|
|
||||||
|
|
@ -51,14 +51,11 @@ const PBKDF2_ITERATIONS = 100_000;
|
||||||
const PBKDF2_KEY_BYTES = 32;
|
const PBKDF2_KEY_BYTES = 32;
|
||||||
const PBKDF2_SALT = 'claude-apikey-storage-v1';
|
const PBKDF2_SALT = 'claude-apikey-storage-v1';
|
||||||
|
|
||||||
export const RUNTIME_MANAGED_API_KEY_ENV_VARS = ['GEMINI_API_KEY'] as const;
|
|
||||||
|
|
||||||
export class ApiKeyService {
|
export class ApiKeyService {
|
||||||
private readonly filePath: string;
|
private readonly filePath: string;
|
||||||
private cache: StoredApiKey[] | null = null;
|
private cache: StoredApiKey[] | null = null;
|
||||||
private aesKey: Buffer | null = null;
|
private aesKey: Buffer | null = null;
|
||||||
private readonly reportedDecryptFailures = new Set<string>();
|
private readonly reportedDecryptFailures = new Set<string>();
|
||||||
private readonly originalProcessEnv = new Map<string, string | undefined>();
|
|
||||||
|
|
||||||
constructor(claudeDir?: string) {
|
constructor(claudeDir?: string) {
|
||||||
const baseDir = claudeDir ?? path.join(os.homedir(), '.claude');
|
const baseDir = claudeDir ?? path.join(os.homedir(), '.claude');
|
||||||
|
|
@ -185,6 +182,16 @@ export class ApiKeyService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async hasPreferred(envVarName: string, projectPath?: string): Promise<boolean> {
|
||||||
|
const keys = await this.readStore();
|
||||||
|
const preferred = this.pickPreferredKey(
|
||||||
|
keys.filter((key) => key.envVarName === envVarName),
|
||||||
|
projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
return Boolean(preferred?.encryptedValue);
|
||||||
|
}
|
||||||
|
|
||||||
async getStorageStatus(): Promise<ApiKeyStorageStatus> {
|
async getStorageStatus(): Promise<ApiKeyStorageStatus> {
|
||||||
const secure = this.isSecureBackend();
|
const secure = this.isSecureBackend();
|
||||||
const backend = this.getBackendName();
|
const backend = this.getBackendName();
|
||||||
|
|
@ -204,31 +211,6 @@ export class ApiKeyService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncProcessEnv(envVarNames: readonly string[]): Promise<void> {
|
|
||||||
if (!envVarNames.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const envVarName of envVarNames) {
|
|
||||||
if (!this.originalProcessEnv.has(envVarName)) {
|
|
||||||
this.originalProcessEnv.set(envVarName, process.env[envVarName]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextValue = (await this.lookupPreferred(envVarName))?.value;
|
|
||||||
if (nextValue && nextValue.trim().length > 0) {
|
|
||||||
process.env[envVarName] = nextValue;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalValue = this.originalProcessEnv.get(envVarName);
|
|
||||||
if (typeof originalValue === 'string' && originalValue.length > 0) {
|
|
||||||
process.env[envVarName] = originalValue;
|
|
||||||
} else {
|
|
||||||
delete process.env[envVarName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Encryption ──────────────────────────────────────────────────────────
|
// ── Encryption ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* Extension services barrel export.
|
* Extension services barrel export.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { ApiKeyService, RUNTIME_MANAGED_API_KEY_ENV_VARS } from './apikeys/ApiKeyService';
|
export { ApiKeyService } from './apikeys/ApiKeyService';
|
||||||
export { GitHubStarsService } from './catalog/GitHubStarsService';
|
export { GitHubStarsService } from './catalog/GitHubStarsService';
|
||||||
export { GlamaMcpEnrichmentService } from './catalog/GlamaMcpEnrichmentService';
|
export { GlamaMcpEnrichmentService } from './catalog/GlamaMcpEnrichmentService';
|
||||||
export { McpCatalogAggregator } from './catalog/McpCatalogAggregator';
|
export { McpCatalogAggregator } from './catalog/McpCatalogAggregator';
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ async function buildManagementCliEnvForBinary(binaryPath: string): Promise<NodeJ
|
||||||
const { env } = await buildProviderAwareCliEnv({
|
const { env } = await buildProviderAwareCliEnv({
|
||||||
binaryPath,
|
binaryPath,
|
||||||
connectionMode: 'augment',
|
connectionMode: 'augment',
|
||||||
|
allowStoredApiKeyDecryption: false,
|
||||||
});
|
});
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ interface OpenCodeRuntimeManifest {
|
||||||
installedAt: string;
|
installedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlatformCandidate {
|
export interface PlatformCandidate {
|
||||||
packageName: string;
|
packageName: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +98,22 @@ export function resolveAppManagedOpenCodeRuntimeBinaryPath(): string | null {
|
||||||
return isAbsoluteExistingFile(manifest?.binaryPath) ? manifest.binaryPath : null;
|
return isAbsoluteExistingFile(manifest?.binaryPath) ? manifest.binaryPath : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath(): Promise<string | null> {
|
||||||
|
const binaryPath = resolveAppManagedOpenCodeRuntimeBinaryPath();
|
||||||
|
if (!binaryPath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await execCli(binaryPath, ['--version'], {
|
||||||
|
timeout: VERSION_TIMEOUT_MS,
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
return binaryPath;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getExecutableName(): string {
|
function getExecutableName(): string {
|
||||||
return process.platform === 'win32' ? 'opencode.exe' : 'opencode';
|
return process.platform === 'win32' ? 'opencode.exe' : 'opencode';
|
||||||
}
|
}
|
||||||
|
|
@ -150,10 +166,12 @@ function isLinuxMuslRuntime(): boolean {
|
||||||
return !header?.glibcVersionRuntime;
|
return !header?.glibcVersionRuntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlatformCandidates(): PlatformCandidate[] {
|
export function getOpenCodeRuntimePlatformCandidates(
|
||||||
const arch = process.arch;
|
platform: NodeJS.Platform = process.platform,
|
||||||
const musl = isLinuxMuslRuntime();
|
arch: string = process.arch,
|
||||||
if (process.platform === 'darwin') {
|
musl: boolean = isLinuxMuslRuntime()
|
||||||
|
): PlatformCandidate[] {
|
||||||
|
if (platform === 'darwin') {
|
||||||
if (arch === 'arm64') return [{ packageName: 'opencode-darwin-arm64', reason: 'macOS arm64' }];
|
if (arch === 'arm64') return [{ packageName: 'opencode-darwin-arm64', reason: 'macOS arm64' }];
|
||||||
if (arch === 'x64') {
|
if (arch === 'x64') {
|
||||||
return [
|
return [
|
||||||
|
|
@ -162,7 +180,7 @@ function getPlatformCandidates(): PlatformCandidate[] {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process.platform === 'linux') {
|
if (platform === 'linux') {
|
||||||
if (arch === 'arm64') {
|
if (arch === 'arm64') {
|
||||||
return musl
|
return musl
|
||||||
? [
|
? [
|
||||||
|
|
@ -191,7 +209,7 @@ function getPlatformCandidates(): PlatformCandidate[] {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process.platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
if (arch === 'arm64')
|
if (arch === 'arm64')
|
||||||
return [{ packageName: 'opencode-windows-arm64', reason: 'Windows arm64' }];
|
return [{ packageName: 'opencode-windows-arm64', reason: 'Windows arm64' }];
|
||||||
if (arch === 'x64') {
|
if (arch === 'x64') {
|
||||||
|
|
@ -201,7 +219,7 @@ function getPlatformCandidates(): PlatformCandidate[] {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`OpenCode app install is not supported on ${process.platform}/${arch}`);
|
throw new Error(`OpenCode app install is not supported on ${platform}/${arch}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchText(url: string): Promise<string> {
|
async function fetchText(url: string): Promise<string> {
|
||||||
|
|
@ -231,7 +249,7 @@ async function fetchPackageMetadata(
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyIntegrity(buffer: Buffer, integrity: string): void {
|
export function verifyOpenCodeRuntimePackageIntegrity(buffer: Buffer, integrity: string): void {
|
||||||
const match = /^sha512-([A-Za-z0-9+/=]+)$/.exec(integrity.trim());
|
const match = /^sha512-([A-Za-z0-9+/=]+)$/.exec(integrity.trim());
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error('OpenCode package integrity is missing sha512 metadata');
|
throw new Error('OpenCode package integrity is missing sha512 metadata');
|
||||||
|
|
@ -320,7 +338,7 @@ function assertSafeTarPath(name: string): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractBinaryFromTarball(tarball: Buffer): Buffer {
|
export function extractOpenCodeRuntimeBinaryFromTarball(tarball: Buffer): Buffer {
|
||||||
const tar = gunzipSync(tarball, { maxOutputLength: MAX_BINARY_BYTES + 1024 * 1024 });
|
const tar = gunzipSync(tarball, { maxOutputLength: MAX_BINARY_BYTES + 1024 * 1024 });
|
||||||
const targetName = `package/bin/${getExecutableName()}`;
|
const targetName = `package/bin/${getExecutableName()}`;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
@ -479,7 +497,7 @@ export class OpenCodeRuntimeInstallerService {
|
||||||
try {
|
try {
|
||||||
this.publishProgress({ phase: 'checking', detail: 'Resolving latest OpenCode package...' });
|
this.publishProgress({ phase: 'checking', detail: 'Resolving latest OpenCode package...' });
|
||||||
const rootMetadata = await fetchPackageMetadata(ROOT_PACKAGE_NAME);
|
const rootMetadata = await fetchPackageMetadata(ROOT_PACKAGE_NAME);
|
||||||
const candidates = getPlatformCandidates();
|
const candidates = getOpenCodeRuntimePlatformCandidates();
|
||||||
const optionalDependencies = rootMetadata.optionalDependencies ?? {};
|
const optionalDependencies = rootMetadata.optionalDependencies ?? {};
|
||||||
const selected = candidates.find((candidate) => optionalDependencies[candidate.packageName]);
|
const selected = candidates.find((candidate) => optionalDependencies[candidate.packageName]);
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
|
|
@ -498,10 +516,10 @@ export class OpenCodeRuntimeInstallerService {
|
||||||
const tarball = await downloadTarball(platformMetadata.dist!.tarball!, (progress) => {
|
const tarball = await downloadTarball(platformMetadata.dist!.tarball!, (progress) => {
|
||||||
this.publishProgress(progress);
|
this.publishProgress(progress);
|
||||||
});
|
});
|
||||||
verifyIntegrity(tarball, platformMetadata.dist!.integrity!);
|
verifyOpenCodeRuntimePackageIntegrity(tarball, platformMetadata.dist!.integrity!);
|
||||||
|
|
||||||
this.publishProgress({ phase: 'installing', detail: 'Extracting OpenCode binary...' });
|
this.publishProgress({ phase: 'installing', detail: 'Extracting OpenCode binary...' });
|
||||||
const binary = extractBinaryFromTarball(tarball);
|
const binary = extractOpenCodeRuntimeBinaryFromTarball(tarball);
|
||||||
const runtimeRoot = getRuntimeRootPath();
|
const runtimeRoot = getRuntimeRootPath();
|
||||||
const tempDir = path.join(runtimeRoot, `installing-${process.pid}-${randomUUID()}`);
|
const tempDir = path.join(runtimeRoot, `installing-${process.pid}-${randomUUID()}`);
|
||||||
const versionDir = path.join(
|
const versionDir = path.join(
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ export class PtyTerminalService {
|
||||||
const { env } = await buildProviderAwareCliEnv({
|
const { env } = await buildProviderAwareCliEnv({
|
||||||
env: options?.env,
|
env: options?.env,
|
||||||
connectionMode: 'augment',
|
connectionMode: 'augment',
|
||||||
|
allowStoredApiKeyDecryption: false,
|
||||||
});
|
});
|
||||||
const shell =
|
const shell =
|
||||||
options?.command ??
|
options?.command ??
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ const logger = createLogger('ClaudeMultimodelBridgeService');
|
||||||
|
|
||||||
const PROVIDER_STATUS_TIMEOUT_MS = 10_000;
|
const PROVIDER_STATUS_TIMEOUT_MS = 10_000;
|
||||||
const PROVIDER_MODELS_TIMEOUT_MS = 10_000;
|
const PROVIDER_MODELS_TIMEOUT_MS = 10_000;
|
||||||
|
const PROVIDER_STATUS_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
||||||
|
const PROVIDER_MODELS_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
||||||
|
|
||||||
interface RuntimeExtensionCapabilityResponse {
|
interface RuntimeExtensionCapabilityResponse {
|
||||||
status?: 'supported' | 'read-only' | 'unsupported';
|
status?: 'supported' | 'read-only' | 'unsupported';
|
||||||
|
|
@ -82,6 +84,7 @@ interface RuntimeProviderModelCatalogItemResponse {
|
||||||
source?: 'anthropic-models-api' | 'app-server' | 'static-fallback';
|
source?: 'anthropic-models-api' | 'app-server' | 'static-fallback';
|
||||||
badgeLabel?: string | null;
|
badgeLabel?: string | null;
|
||||||
statusMessage?: string | null;
|
statusMessage?: string | null;
|
||||||
|
metadata?: Record<string, unknown> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RuntimeProviderModelCatalogResponse {
|
interface RuntimeProviderModelCatalogResponse {
|
||||||
|
|
@ -478,6 +481,21 @@ function collectRuntimeReasoningEfforts(values?: string[]): CliProviderReasoning
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapRuntimeProviderModelMetadata(
|
||||||
|
metadata?: Record<string, unknown> | null
|
||||||
|
): NonNullable<CliProviderStatus['modelCatalog']>['models'][number]['metadata'] {
|
||||||
|
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const context = metadata.context;
|
||||||
|
return {
|
||||||
|
cost: metadata.cost ?? null,
|
||||||
|
context: typeof context === 'number' && Number.isFinite(context) ? context : null,
|
||||||
|
limits: metadata.limits ?? null,
|
||||||
|
free: metadata.free === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function mapRuntimeProviderModelCatalog(
|
function mapRuntimeProviderModelCatalog(
|
||||||
providerId: CliProviderId,
|
providerId: CliProviderId,
|
||||||
modelCatalog?: RuntimeProviderModelCatalogResponse | null
|
modelCatalog?: RuntimeProviderModelCatalogResponse | null
|
||||||
|
|
@ -540,6 +558,7 @@ function mapRuntimeProviderModelCatalog(
|
||||||
source: itemSource,
|
source: itemSource,
|
||||||
badgeLabel: model.badgeLabel ?? null,
|
badgeLabel: model.badgeLabel ?? null,
|
||||||
statusMessage: model.statusMessage ?? null,
|
statusMessage: model.statusMessage ?? null,
|
||||||
|
metadata: mapRuntimeProviderModelMetadata(model.metadata),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
|
|
@ -605,14 +624,18 @@ export class ClaudeMultimodelBridgeService {
|
||||||
private async buildCliEnv(
|
private async buildCliEnv(
|
||||||
binaryPath: string
|
binaryPath: string
|
||||||
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
||||||
return buildProviderAwareCliEnv({ binaryPath });
|
return buildProviderAwareCliEnv({ binaryPath, allowStoredApiKeyDecryption: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildProviderCliEnv(
|
private async buildProviderCliEnv(
|
||||||
binaryPath: string,
|
binaryPath: string,
|
||||||
providerId: CliProviderId
|
providerId: CliProviderId
|
||||||
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
||||||
return buildProviderAwareCliEnv({ binaryPath, providerId });
|
return buildProviderAwareCliEnv({
|
||||||
|
binaryPath,
|
||||||
|
providerId,
|
||||||
|
allowStoredApiKeyDecryption: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private isUnifiedRuntimeUnsupported(error: unknown): boolean {
|
private isUnifiedRuntimeUnsupported(error: unknown): boolean {
|
||||||
|
|
@ -769,6 +792,7 @@ export class ClaudeMultimodelBridgeService {
|
||||||
['runtime', 'status', '--json', '--provider', providerId],
|
['runtime', 'status', '--json', '--provider', providerId],
|
||||||
{
|
{
|
||||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||||
|
maxBuffer: PROVIDER_STATUS_MAX_BUFFER_BYTES,
|
||||||
env,
|
env,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -850,6 +874,7 @@ export class ClaudeMultimodelBridgeService {
|
||||||
['runtime', 'verify', '--json', '--provider', 'opencode'],
|
['runtime', 'verify', '--json', '--provider', 'opencode'],
|
||||||
{
|
{
|
||||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||||
|
maxBuffer: PROVIDER_STATUS_MAX_BUFFER_BYTES,
|
||||||
env,
|
env,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -1048,6 +1073,7 @@ export class ClaudeMultimodelBridgeService {
|
||||||
['model', 'list', '--json', '--provider', 'all'],
|
['model', 'list', '--json', '--provider', 'all'],
|
||||||
{
|
{
|
||||||
timeout: PROVIDER_MODELS_TIMEOUT_MS,
|
timeout: PROVIDER_MODELS_TIMEOUT_MS,
|
||||||
|
maxBuffer: PROVIDER_MODELS_MAX_BUFFER_BYTES,
|
||||||
env,
|
env,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -1119,6 +1145,7 @@ export class ClaudeMultimodelBridgeService {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execCli(binaryPath, ['runtime', 'status', '--json'], {
|
const { stdout } = await execCli(binaryPath, ['runtime', 'status', '--json'], {
|
||||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||||
|
maxBuffer: PROVIDER_STATUS_MAX_BUFFER_BYTES,
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
const parsed = extractJsonObject<UnifiedRuntimeStatusResponse>(stdout);
|
const parsed = extractJsonObject<UnifiedRuntimeStatusResponse>(stdout);
|
||||||
|
|
@ -1145,10 +1172,12 @@ export class ClaudeMultimodelBridgeService {
|
||||||
const [statusResult, modelsResult] = await Promise.allSettled([
|
const [statusResult, modelsResult] = await Promise.allSettled([
|
||||||
execCli(binaryPath, ['auth', 'status', '--json', '--provider', 'all'], {
|
execCli(binaryPath, ['auth', 'status', '--json', '--provider', 'all'], {
|
||||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||||
|
maxBuffer: PROVIDER_STATUS_MAX_BUFFER_BYTES,
|
||||||
env,
|
env,
|
||||||
}),
|
}),
|
||||||
execCli(binaryPath, ['model', 'list', '--json', '--provider', 'all'], {
|
execCli(binaryPath, ['model', 'list', '--json', '--provider', 'all'], {
|
||||||
timeout: PROVIDER_MODELS_TIMEOUT_MS,
|
timeout: PROVIDER_MODELS_TIMEOUT_MS,
|
||||||
|
maxBuffer: PROVIDER_MODELS_MAX_BUFFER_BYTES,
|
||||||
env,
|
env,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,7 @@ export class CliProviderModelAvailabilityService {
|
||||||
cliEnvPromise: buildProviderAwareCliEnv({
|
cliEnvPromise: buildProviderAwareCliEnv({
|
||||||
binaryPath: context.binaryPath,
|
binaryPath: context.binaryPath,
|
||||||
providerId: context.provider.providerId,
|
providerId: context.provider.providerId,
|
||||||
|
allowStoredApiKeyDecryption: false,
|
||||||
}).then((result) => ({
|
}).then((result) => ({
|
||||||
env: result.env,
|
env: result.env,
|
||||||
providerArgs: result.providerArgs ?? [],
|
providerArgs: result.providerArgs ?? [],
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@ type ExternalCredential = {
|
||||||
value: string;
|
value: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
|
interface StoredApiKeyAccessOptions {
|
||||||
|
allowStoredApiKeyDecryption?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const PROVIDER_CAPABILITIES: Record<
|
const PROVIDER_CAPABILITIES: Record<
|
||||||
CliProviderId,
|
CliProviderId,
|
||||||
Pick<CliProviderConnectionInfo, 'supportsOAuth' | 'supportsApiKey' | 'configurableAuthModes'>
|
Pick<CliProviderConnectionInfo, 'supportsOAuth' | 'supportsApiKey' | 'configurableAuthModes'>
|
||||||
|
|
@ -258,7 +262,8 @@ export class ProviderConnectionService {
|
||||||
async applyConfiguredConnectionEnv(
|
async applyConfiguredConnectionEnv(
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
providerId: CliProviderId,
|
providerId: CliProviderId,
|
||||||
runtimeBackendOverride?: string | null
|
runtimeBackendOverride?: string | null,
|
||||||
|
options?: StoredApiKeyAccessOptions
|
||||||
): Promise<NodeJS.ProcessEnv> {
|
): Promise<NodeJS.ProcessEnv> {
|
||||||
if (providerId === 'anthropic') {
|
if (providerId === 'anthropic') {
|
||||||
const authMode = this.getConfiguredAuthMode(providerId);
|
const authMode = this.getConfiguredAuthMode(providerId);
|
||||||
|
|
@ -272,7 +277,7 @@ export class ProviderConnectionService {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedKey = await this.apiKeyService.lookupPreferred('ANTHROPIC_API_KEY');
|
const storedKey = await this.lookupStoredApiKeyValue('ANTHROPIC_API_KEY', options);
|
||||||
if (storedKey?.value.trim()) {
|
if (storedKey?.value.trim()) {
|
||||||
env.ANTHROPIC_API_KEY = storedKey.value;
|
env.ANTHROPIC_API_KEY = storedKey.value;
|
||||||
delete env.ANTHROPIC_AUTH_TOKEN;
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
||||||
|
|
@ -288,6 +293,14 @@ export class ProviderConnectionService {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (providerId === 'gemini') {
|
||||||
|
const storedKey = await this.lookupStoredApiKeyValue('GEMINI_API_KEY', options);
|
||||||
|
if (storedKey?.value.trim()) {
|
||||||
|
env.GEMINI_API_KEY = storedKey.value;
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
if (providerId !== 'codex') {
|
if (providerId !== 'codex') {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +323,7 @@ export class ProviderConnectionService {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedApiKey = await this.resolveCodexApiKeyValue(env, runtimeBackendOverride);
|
const resolvedApiKey = await this.resolveCodexApiKeyValue(env, runtimeBackendOverride, options);
|
||||||
if (readiness.effectiveAuthMode === 'api_key' && resolvedApiKey) {
|
if (readiness.effectiveAuthMode === 'api_key' && resolvedApiKey) {
|
||||||
env.OPENAI_API_KEY = resolvedApiKey;
|
env.OPENAI_API_KEY = resolvedApiKey;
|
||||||
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
|
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
|
||||||
|
|
@ -327,10 +340,13 @@ export class ProviderConnectionService {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyAllConfiguredConnectionEnv(env: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv> {
|
async applyAllConfiguredConnectionEnv(
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
options?: StoredApiKeyAccessOptions
|
||||||
|
): Promise<NodeJS.ProcessEnv> {
|
||||||
let nextEnv = env;
|
let nextEnv = env;
|
||||||
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
|
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
|
||||||
nextEnv = await this.applyConfiguredConnectionEnv(nextEnv, providerId);
|
nextEnv = await this.applyConfiguredConnectionEnv(nextEnv, providerId, undefined, options);
|
||||||
}
|
}
|
||||||
return nextEnv;
|
return nextEnv;
|
||||||
}
|
}
|
||||||
|
|
@ -338,20 +354,29 @@ export class ProviderConnectionService {
|
||||||
async augmentConfiguredConnectionEnv(
|
async augmentConfiguredConnectionEnv(
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
providerId: CliProviderId,
|
providerId: CliProviderId,
|
||||||
runtimeBackendOverride?: string | null
|
runtimeBackendOverride?: string | null,
|
||||||
|
options?: StoredApiKeyAccessOptions
|
||||||
): Promise<NodeJS.ProcessEnv> {
|
): Promise<NodeJS.ProcessEnv> {
|
||||||
if (providerId === 'anthropic') {
|
if (providerId === 'anthropic') {
|
||||||
if (this.getConfiguredAuthMode(providerId) !== 'api_key') {
|
if (this.getConfiguredAuthMode(providerId) !== 'api_key') {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedKey = await this.apiKeyService.lookupPreferred('ANTHROPIC_API_KEY');
|
const storedKey = await this.lookupStoredApiKeyValue('ANTHROPIC_API_KEY', options);
|
||||||
if (storedKey?.value.trim()) {
|
if (storedKey?.value.trim()) {
|
||||||
env.ANTHROPIC_API_KEY = storedKey.value;
|
env.ANTHROPIC_API_KEY = storedKey.value;
|
||||||
}
|
}
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (providerId === 'gemini') {
|
||||||
|
const storedKey = await this.lookupStoredApiKeyValue('GEMINI_API_KEY', options);
|
||||||
|
if (storedKey?.value.trim()) {
|
||||||
|
env.GEMINI_API_KEY = storedKey.value;
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
if (providerId !== 'codex') {
|
if (providerId !== 'codex') {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
@ -374,7 +399,7 @@ export class ProviderConnectionService {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedApiKey = await this.resolveCodexApiKeyValue(env, runtimeBackendOverride);
|
const resolvedApiKey = await this.resolveCodexApiKeyValue(env, runtimeBackendOverride, options);
|
||||||
if (readiness.effectiveAuthMode === 'api_key' && resolvedApiKey) {
|
if (readiness.effectiveAuthMode === 'api_key' && resolvedApiKey) {
|
||||||
env.OPENAI_API_KEY = resolvedApiKey;
|
env.OPENAI_API_KEY = resolvedApiKey;
|
||||||
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
|
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
|
||||||
|
|
@ -386,10 +411,13 @@ export class ProviderConnectionService {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
async augmentAllConfiguredConnectionEnv(env: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv> {
|
async augmentAllConfiguredConnectionEnv(
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
options?: StoredApiKeyAccessOptions
|
||||||
|
): Promise<NodeJS.ProcessEnv> {
|
||||||
let nextEnv = env;
|
let nextEnv = env;
|
||||||
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
|
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
|
||||||
nextEnv = await this.augmentConfiguredConnectionEnv(nextEnv, providerId);
|
nextEnv = await this.augmentConfiguredConnectionEnv(nextEnv, providerId, undefined, options);
|
||||||
}
|
}
|
||||||
return nextEnv;
|
return nextEnv;
|
||||||
}
|
}
|
||||||
|
|
@ -408,8 +436,7 @@ export class ProviderConnectionService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedKey = await this.apiKeyService.lookupPreferred('ANTHROPIC_API_KEY');
|
if (await this.hasStoredApiKey('ANTHROPIC_API_KEY')) {
|
||||||
if (storedKey?.value.trim()) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -654,7 +681,7 @@ export class ProviderConnectionService {
|
||||||
|
|
||||||
async getConnectionInfo(providerId: CliProviderId): Promise<CliProviderConnectionInfo> {
|
async getConnectionInfo(providerId: CliProviderId): Promise<CliProviderConnectionInfo> {
|
||||||
const capabilities = PROVIDER_CAPABILITIES[providerId];
|
const capabilities = PROVIDER_CAPABILITIES[providerId];
|
||||||
const storedApiKey = await this.getStoredApiKey(providerId);
|
const hasStoredApiKey = await this.hasStoredProviderApiKey(providerId);
|
||||||
const externalCredential = this.getExternalCredential(providerId);
|
const externalCredential = this.getExternalCredential(providerId);
|
||||||
const codexSnapshot = providerId === 'codex' ? await this.getCodexAccountSnapshot() : null;
|
const codexSnapshot = providerId === 'codex' ? await this.getCodexAccountSnapshot() : null;
|
||||||
const configurableAuthModes = capabilities.configurableAuthModes;
|
const configurableAuthModes = capabilities.configurableAuthModes;
|
||||||
|
|
@ -665,11 +692,11 @@ export class ProviderConnectionService {
|
||||||
const apiKeyConfigured =
|
const apiKeyConfigured =
|
||||||
providerId === 'codex'
|
providerId === 'codex'
|
||||||
? (codexSnapshot?.apiKey.available ?? false)
|
? (codexSnapshot?.apiKey.available ?? false)
|
||||||
: Boolean(storedApiKey?.value.trim() || externalCredential?.value.trim());
|
: Boolean(hasStoredApiKey || externalCredential?.value.trim());
|
||||||
const apiKeySource =
|
const apiKeySource =
|
||||||
providerId === 'codex'
|
providerId === 'codex'
|
||||||
? (codexSnapshot?.apiKey.source ?? null)
|
? (codexSnapshot?.apiKey.source ?? null)
|
||||||
: storedApiKey?.value.trim()
|
: hasStoredApiKey
|
||||||
? 'stored'
|
? 'stored'
|
||||||
: externalCredential?.value.trim()
|
: externalCredential?.value.trim()
|
||||||
? 'environment'
|
? 'environment'
|
||||||
|
|
@ -677,7 +704,7 @@ export class ProviderConnectionService {
|
||||||
const apiKeySourceLabel =
|
const apiKeySourceLabel =
|
||||||
providerId === 'codex'
|
providerId === 'codex'
|
||||||
? (codexSnapshot?.apiKey.sourceLabel ?? null)
|
? (codexSnapshot?.apiKey.sourceLabel ?? null)
|
||||||
: storedApiKey?.value.trim()
|
: hasStoredApiKey
|
||||||
? 'Stored in app'
|
? 'Stored in app'
|
||||||
: (externalCredential?.label ?? null);
|
: (externalCredential?.label ?? null);
|
||||||
|
|
||||||
|
|
@ -709,11 +736,33 @@ export class ProviderConnectionService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getStoredApiKey(
|
private async hasStoredProviderApiKey(providerId: CliProviderId): Promise<boolean> {
|
||||||
providerId: CliProviderId
|
|
||||||
): Promise<{ envVarName: string; value: string } | null> {
|
|
||||||
const envVarName = PROVIDER_API_KEY_ENV_VARS[providerId];
|
const envVarName = PROVIDER_API_KEY_ENV_VARS[providerId];
|
||||||
if (!envVarName) {
|
if (!envVarName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hasStoredApiKey(envVarName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hasStoredApiKey(envVarName: string): Promise<boolean> {
|
||||||
|
const service = this.apiKeyService as ApiKeyService & {
|
||||||
|
hasPreferred?: (envVarName: string) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof service.hasPreferred === 'function') {
|
||||||
|
return service.hasPreferred(envVarName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedKey = await service.lookupPreferred(envVarName);
|
||||||
|
return Boolean(storedKey?.value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async lookupStoredApiKeyValue(
|
||||||
|
envVarName: string,
|
||||||
|
options?: StoredApiKeyAccessOptions
|
||||||
|
): Promise<{ envVarName: string; value: string } | null> {
|
||||||
|
if (options?.allowStoredApiKeyDecryption === false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -736,17 +785,17 @@ export class ProviderConnectionService {
|
||||||
(this.configManager.getConfig().providerConnections.codex.preferredAuthMode as
|
(this.configManager.getConfig().providerConnections.codex.preferredAuthMode as
|
||||||
| CodexAccountAuthMode
|
| CodexAccountAuthMode
|
||||||
| undefined) ?? 'auto';
|
| undefined) ?? 'auto';
|
||||||
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
const hasStoredOpenAiKey = await this.hasStoredApiKey('OPENAI_API_KEY');
|
||||||
const externalCredential = this.getExternalCredential('codex');
|
const externalCredential = this.getExternalCredential('codex');
|
||||||
const apiKeyAvailable = Boolean(storedKey?.value.trim() || externalCredential?.value.trim());
|
const apiKeyAvailable = Boolean(hasStoredOpenAiKey || externalCredential?.value.trim());
|
||||||
const apiKey = {
|
const apiKey = {
|
||||||
available: apiKeyAvailable,
|
available: apiKeyAvailable,
|
||||||
source: storedKey?.value.trim()
|
source: hasStoredOpenAiKey
|
||||||
? 'stored'
|
? 'stored'
|
||||||
: externalCredential?.value.trim()
|
: externalCredential?.value.trim()
|
||||||
? 'environment'
|
? 'environment'
|
||||||
: null,
|
: null,
|
||||||
sourceLabel: storedKey?.value.trim() ? 'Stored in app' : (externalCredential?.label ?? null),
|
sourceLabel: hasStoredOpenAiKey ? 'Stored in app' : (externalCredential?.label ?? null),
|
||||||
} satisfies CodexAccountSnapshotDto['apiKey'];
|
} satisfies CodexAccountSnapshotDto['apiKey'];
|
||||||
const readiness = evaluateCodexLaunchReadiness({
|
const readiness = evaluateCodexLaunchReadiness({
|
||||||
preferredAuthMode,
|
preferredAuthMode,
|
||||||
|
|
@ -786,10 +835,11 @@ export class ProviderConnectionService {
|
||||||
|
|
||||||
private async resolveCodexApiKeyValue(
|
private async resolveCodexApiKeyValue(
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
runtimeBackendOverride?: string | null
|
runtimeBackendOverride?: string | null,
|
||||||
|
options?: StoredApiKeyAccessOptions
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
||||||
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
const storedKey = await this.lookupStoredApiKeyValue('OPENAI_API_KEY', options);
|
||||||
const existingOpenAiKey =
|
const existingOpenAiKey =
|
||||||
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
|
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
|
||||||
? env.OPENAI_API_KEY
|
? env.OPENAI_API_KEY
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { getCachedShellEnv } from '@main/utils/shellEnv';
|
import { getCachedShellEnv } from '@main/utils/shellEnv';
|
||||||
|
|
||||||
import { resolveAppManagedOpenCodeRuntimeBinaryPath } from '../infrastructure/OpenCodeRuntimeInstallerService';
|
import { resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath } from '../infrastructure/OpenCodeRuntimeInstallerService';
|
||||||
|
|
||||||
import { buildRuntimeBaseEnv } from './buildRuntimeBaseEnv';
|
import { buildRuntimeBaseEnv } from './buildRuntimeBaseEnv';
|
||||||
import { providerConnectionService } from './ProviderConnectionService';
|
import { providerConnectionService } from './ProviderConnectionService';
|
||||||
|
|
@ -16,6 +16,7 @@ export interface ProviderAwareCliEnvOptions {
|
||||||
shellEnv?: NodeJS.ProcessEnv | null;
|
shellEnv?: NodeJS.ProcessEnv | null;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
connectionMode?: 'strict' | 'augment';
|
connectionMode?: 'strict' | 'augment';
|
||||||
|
allowStoredApiKeyDecryption?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderAwareCliEnvResult {
|
export interface ProviderAwareCliEnvResult {
|
||||||
|
|
@ -28,6 +29,10 @@ export async function buildProviderAwareCliEnv(
|
||||||
options: ProviderAwareCliEnvOptions = {}
|
options: ProviderAwareCliEnvOptions = {}
|
||||||
): Promise<ProviderAwareCliEnvResult> {
|
): Promise<ProviderAwareCliEnvResult> {
|
||||||
const connectionMode = options.connectionMode ?? 'strict';
|
const connectionMode = options.connectionMode ?? 'strict';
|
||||||
|
const storedApiKeyAccessArgs =
|
||||||
|
options.allowStoredApiKeyDecryption === undefined
|
||||||
|
? []
|
||||||
|
: [{ allowStoredApiKeyDecryption: options.allowStoredApiKeyDecryption }];
|
||||||
const shellEnv = options.shellEnv ?? getCachedShellEnv() ?? {};
|
const shellEnv = options.shellEnv ?? getCachedShellEnv() ?? {};
|
||||||
const { env, resolvedProviderId } = buildRuntimeBaseEnv({
|
const { env, resolvedProviderId } = buildRuntimeBaseEnv({
|
||||||
binaryPath: options.binaryPath,
|
binaryPath: options.binaryPath,
|
||||||
|
|
@ -36,7 +41,7 @@ export async function buildProviderAwareCliEnv(
|
||||||
shellEnv,
|
shellEnv,
|
||||||
env: options.env,
|
env: options.env,
|
||||||
});
|
});
|
||||||
const appManagedOpenCodeBinary = resolveAppManagedOpenCodeRuntimeBinaryPath();
|
const appManagedOpenCodeBinary = await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath();
|
||||||
if (
|
if (
|
||||||
appManagedOpenCodeBinary &&
|
appManagedOpenCodeBinary &&
|
||||||
!env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH &&
|
!env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH &&
|
||||||
|
|
@ -53,7 +58,8 @@ export async function buildProviderAwareCliEnv(
|
||||||
await providerConnectionService.augmentConfiguredConnectionEnv(
|
await providerConnectionService.augmentConfiguredConnectionEnv(
|
||||||
env,
|
env,
|
||||||
resolvedProviderId,
|
resolvedProviderId,
|
||||||
options.providerBackendId
|
options.providerBackendId,
|
||||||
|
...storedApiKeyAccessArgs
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
env,
|
env,
|
||||||
|
|
@ -65,7 +71,8 @@ export async function buildProviderAwareCliEnv(
|
||||||
await providerConnectionService.applyConfiguredConnectionEnv(
|
await providerConnectionService.applyConfiguredConnectionEnv(
|
||||||
env,
|
env,
|
||||||
resolvedProviderId,
|
resolvedProviderId,
|
||||||
options.providerBackendId
|
options.providerBackendId,
|
||||||
|
...storedApiKeyAccessArgs
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -87,7 +94,10 @@ export async function buildProviderAwareCliEnv(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionMode === 'augment') {
|
if (connectionMode === 'augment') {
|
||||||
await providerConnectionService.augmentAllConfiguredConnectionEnv(env);
|
await providerConnectionService.augmentAllConfiguredConnectionEnv(
|
||||||
|
env,
|
||||||
|
...storedApiKeyAccessArgs
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
env,
|
env,
|
||||||
connectionIssues: {},
|
connectionIssues: {},
|
||||||
|
|
@ -95,7 +105,7 @@ export async function buildProviderAwareCliEnv(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await providerConnectionService.applyAllConfiguredConnectionEnv(env);
|
await providerConnectionService.applyAllConfiguredConnectionEnv(env, ...storedApiKeyAccessArgs);
|
||||||
return {
|
return {
|
||||||
env,
|
env,
|
||||||
connectionIssues: await providerConnectionService.getConfiguredConnectionIssues(env),
|
connectionIssues: await providerConnectionService.getConfiguredConnectionIssues(env),
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ export interface TeamLaunchFailureArtifactPackResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LaunchFailureArtifactClassificationCode =
|
export type LaunchFailureArtifactClassificationCode =
|
||||||
|
| 'workspace_trust_required'
|
||||||
| 'transport_rejected'
|
| 'transport_rejected'
|
||||||
| 'stdin_missing'
|
| 'stdin_missing'
|
||||||
| 'provider_quota'
|
| 'provider_quota'
|
||||||
|
|
@ -215,6 +216,13 @@ function firstEvidence(parts: readonly string[], pattern: RegExp): string[] {
|
||||||
return evidence;
|
return evidence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WORKSPACE_TRUST_FAILURE_PATTERN =
|
||||||
|
/workspace trust is not accepted|cannot start in headless process runtime because workspace trust|open that workspace once interactively and accept trust/i;
|
||||||
|
|
||||||
|
export function isWorkspaceTrustLaunchFailureText(value: string): boolean {
|
||||||
|
return WORKSPACE_TRUST_FAILURE_PATTERN.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
export function classifyLaunchFailureArtifact(
|
export function classifyLaunchFailureArtifact(
|
||||||
input: TeamLaunchFailureArtifactPackInput
|
input: TeamLaunchFailureArtifactPackInput
|
||||||
): LaunchFailureArtifactClassification {
|
): LaunchFailureArtifactClassification {
|
||||||
|
|
@ -225,6 +233,11 @@ export function classifyLaunchFailureArtifact(
|
||||||
confidence: number;
|
confidence: number;
|
||||||
pattern: RegExp;
|
pattern: RegExp;
|
||||||
}[] = [
|
}[] = [
|
||||||
|
{
|
||||||
|
code: 'workspace_trust_required',
|
||||||
|
confidence: 0.96,
|
||||||
|
pattern: WORKSPACE_TRUST_FAILURE_PATTERN,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
code: 'transport_rejected',
|
code: 'transport_rejected',
|
||||||
confidence: 0.95,
|
confidence: 0.95,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createLogger } from '@shared/utils/logger';
|
|
||||||
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||||
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { watch } from 'chokidar';
|
import { watch } from 'chokidar';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
|
|
@ -14,7 +14,6 @@ import {
|
||||||
BOARD_TASK_CHANGES_DIRNAME,
|
BOARD_TASK_CHANGES_DIRNAME,
|
||||||
BOARD_TASK_LOG_FRESHNESS_DIRNAME,
|
BOARD_TASK_LOG_FRESHNESS_DIRNAME,
|
||||||
BOARD_TASK_LOG_FRESHNESS_FILE_SUFFIX,
|
BOARD_TASK_LOG_FRESHNESS_FILE_SUFFIX,
|
||||||
TEAM_TASK_LOG_FRESHNESS_DIRNAME,
|
|
||||||
classifyLogSourceWatcherEvent,
|
classifyLogSourceWatcherEvent,
|
||||||
getRelativeLogSourceParts,
|
getRelativeLogSourceParts,
|
||||||
isAgentTranscriptFileName,
|
isAgentTranscriptFileName,
|
||||||
|
|
@ -22,6 +21,7 @@ import {
|
||||||
MAX_PENDING_UNKNOWN_ROOT_SESSIONS,
|
MAX_PENDING_UNKNOWN_ROOT_SESSIONS,
|
||||||
normalizeLogSourceSessionId,
|
normalizeLogSourceSessionId,
|
||||||
PENDING_UNKNOWN_ROOT_SESSION_TTL_MS,
|
PENDING_UNKNOWN_ROOT_SESSION_TTL_MS,
|
||||||
|
TEAM_TASK_LOG_FRESHNESS_DIRNAME,
|
||||||
} from './teamLogSourceWatchScope';
|
} from './teamLogSourceWatchScope';
|
||||||
|
|
||||||
import type { TeamLogSourceLiveContext, TeamMemberLogsFinder } from './TeamMemberLogsFinder';
|
import type { TeamLogSourceLiveContext, TeamMemberLogsFinder } from './TeamMemberLogsFinder';
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,11 @@ interface ProjectSessionDiscovery {
|
||||||
knownMembers: Set<string>;
|
knownMembers: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProjectSessionDiscoveryOptions {
|
||||||
|
forceRefresh?: boolean;
|
||||||
|
includeTeamSubagentSessionDiscovery?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface TaskMentionIndex {
|
interface TaskMentionIndex {
|
||||||
exactTaskIds: Set<string>;
|
exactTaskIds: Set<string>;
|
||||||
lowerTaskIds: Set<string>;
|
lowerTaskIds: Set<string>;
|
||||||
|
|
@ -136,6 +141,7 @@ type FindRecentMemberLogFileRefsOptions =
|
||||||
| {
|
| {
|
||||||
mtimeSinceMs?: number | null;
|
mtimeSinceMs?: number | null;
|
||||||
forceRefresh?: boolean;
|
forceRefresh?: boolean;
|
||||||
|
includeTeamSubagentSessionDiscovery?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TeamLogSourceLiveContext {
|
export interface TeamLogSourceLiveContext {
|
||||||
|
|
@ -193,6 +199,14 @@ function collectTaskFreshnessRootDirs(candidates: readonly unknown[]): string[]
|
||||||
return roots;
|
return roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildProjectSessionDiscoveryCacheKey(
|
||||||
|
teamName: string,
|
||||||
|
options?: ProjectSessionDiscoveryOptions
|
||||||
|
): string {
|
||||||
|
const subagentMode = options?.includeTeamSubagentSessionDiscovery === false ? 'known' : 'full';
|
||||||
|
return `${teamName}\0${subagentMode}`;
|
||||||
|
}
|
||||||
|
|
||||||
export class TeamMemberLogsFinder {
|
export class TeamMemberLogsFinder {
|
||||||
private readonly taskMentionIndexCache = new Map<string, TaskMentionIndex>();
|
private readonly taskMentionIndexCache = new Map<string, TaskMentionIndex>();
|
||||||
private readonly taskMentionIndexInFlight = new Map<string, Promise<TaskMentionIndex>>();
|
private readonly taskMentionIndexInFlight = new Map<string, Promise<TaskMentionIndex>>();
|
||||||
|
|
@ -200,6 +214,10 @@ export class TeamMemberLogsFinder {
|
||||||
string,
|
string,
|
||||||
SubagentAttribution | RootSessionAttribution | null
|
SubagentAttribution | RootSessionAttribution | null
|
||||||
>();
|
>();
|
||||||
|
private readonly attributionInFlight = new Map<
|
||||||
|
string,
|
||||||
|
Promise<SubagentAttribution | RootSessionAttribution | null>
|
||||||
|
>();
|
||||||
private readonly discoveryCache = new Map<
|
private readonly discoveryCache = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
|
|
@ -209,7 +227,11 @@ export class TeamMemberLogsFinder {
|
||||||
>();
|
>();
|
||||||
private readonly discoveryInFlight = new Map<
|
private readonly discoveryInFlight = new Map<
|
||||||
string,
|
string,
|
||||||
{ generation: number; promise: Promise<ProjectSessionDiscovery | null> }
|
{
|
||||||
|
generation: number;
|
||||||
|
promise: Promise<ProjectSessionDiscovery | null>;
|
||||||
|
forceRefresh: boolean;
|
||||||
|
}
|
||||||
>();
|
>();
|
||||||
private readonly discoveryGenerationByTeam = new Map<string, number>();
|
private readonly discoveryGenerationByTeam = new Map<string, number>();
|
||||||
|
|
||||||
|
|
@ -979,10 +1001,16 @@ export class TeamMemberLogsFinder {
|
||||||
): Promise<MemberLogFileRef[]> {
|
): Promise<MemberLogFileRef[]> {
|
||||||
const parsedOptions =
|
const parsedOptions =
|
||||||
typeof options === 'number' || options === null
|
typeof options === 'number' || options === null
|
||||||
? { mtimeSinceMs: options ?? null, forceRefresh: false }
|
? {
|
||||||
|
mtimeSinceMs: options ?? null,
|
||||||
|
forceRefresh: false,
|
||||||
|
includeTeamSubagentSessionDiscovery: true,
|
||||||
|
}
|
||||||
: {
|
: {
|
||||||
mtimeSinceMs: options?.mtimeSinceMs ?? null,
|
mtimeSinceMs: options?.mtimeSinceMs ?? null,
|
||||||
forceRefresh: options?.forceRefresh === true,
|
forceRefresh: options?.forceRefresh === true,
|
||||||
|
includeTeamSubagentSessionDiscovery:
|
||||||
|
options?.includeTeamSubagentSessionDiscovery !== false,
|
||||||
};
|
};
|
||||||
const requestedMembersByKey = new Map<string, string>();
|
const requestedMembersByKey = new Map<string, string>();
|
||||||
for (const memberName of memberNames) {
|
for (const memberName of memberNames) {
|
||||||
|
|
@ -1001,6 +1029,7 @@ export class TeamMemberLogsFinder {
|
||||||
|
|
||||||
const discovery = await this.discoverProjectSessions(teamName, {
|
const discovery = await this.discoverProjectSessions(teamName, {
|
||||||
forceRefresh: parsedOptions.forceRefresh,
|
forceRefresh: parsedOptions.forceRefresh,
|
||||||
|
includeTeamSubagentSessionDiscovery: parsedOptions.includeTeamSubagentSessionDiscovery,
|
||||||
});
|
});
|
||||||
if (!discovery) {
|
if (!discovery) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -1147,41 +1176,50 @@ export class TeamMemberLogsFinder {
|
||||||
|
|
||||||
private async discoverProjectSessions(
|
private async discoverProjectSessions(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
options?: { forceRefresh?: boolean }
|
options?: ProjectSessionDiscoveryOptions
|
||||||
): Promise<ProjectSessionDiscovery | null> {
|
): Promise<ProjectSessionDiscovery | null> {
|
||||||
let generation = this.discoveryGenerationByTeam.get(teamName) ?? 0;
|
const cacheKey = buildProjectSessionDiscoveryCacheKey(teamName, options);
|
||||||
|
let generation = this.discoveryGenerationByTeam.get(cacheKey) ?? 0;
|
||||||
if (options?.forceRefresh) {
|
if (options?.forceRefresh) {
|
||||||
|
const inFlight = this.discoveryInFlight.get(cacheKey);
|
||||||
|
if (inFlight?.forceRefresh === true) {
|
||||||
|
return inFlight.promise;
|
||||||
|
}
|
||||||
generation += 1;
|
generation += 1;
|
||||||
this.discoveryGenerationByTeam.set(teamName, generation);
|
this.discoveryGenerationByTeam.set(cacheKey, generation);
|
||||||
this.discoveryCache.delete(teamName);
|
this.discoveryCache.delete(cacheKey);
|
||||||
this.discoveryInFlight.delete(teamName);
|
|
||||||
} else {
|
} else {
|
||||||
// Check discovery cache — avoids re-reading config/dirs within rapid successive calls
|
// Check discovery cache — avoids re-reading config/dirs within rapid successive calls
|
||||||
const cached = this.discoveryCache.get(teamName);
|
const cached = this.discoveryCache.get(cacheKey);
|
||||||
if (cached && cached.expiresAt > Date.now()) {
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
return cached.result;
|
return cached.result;
|
||||||
}
|
}
|
||||||
const inFlight = this.discoveryInFlight.get(teamName);
|
const inFlight = this.discoveryInFlight.get(cacheKey);
|
||||||
if (inFlight) {
|
if (inFlight) {
|
||||||
return inFlight.promise;
|
return inFlight.promise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = this.loadProjectSessionDiscovery(teamName, options, generation).finally(() => {
|
const promise = this.loadProjectSessionDiscovery(teamName, options, generation).finally(() => {
|
||||||
const current = this.discoveryInFlight.get(teamName);
|
const current = this.discoveryInFlight.get(cacheKey);
|
||||||
if (current?.promise === promise) {
|
if (current?.promise === promise) {
|
||||||
this.discoveryInFlight.delete(teamName);
|
this.discoveryInFlight.delete(cacheKey);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.discoveryInFlight.set(teamName, { generation, promise });
|
this.discoveryInFlight.set(cacheKey, {
|
||||||
|
generation,
|
||||||
|
promise,
|
||||||
|
forceRefresh: options?.forceRefresh === true,
|
||||||
|
});
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadProjectSessionDiscovery(
|
private async loadProjectSessionDiscovery(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
options: { forceRefresh?: boolean } | undefined,
|
options: ProjectSessionDiscoveryOptions | undefined,
|
||||||
generation: number
|
generation: number
|
||||||
): Promise<ProjectSessionDiscovery | null> {
|
): Promise<ProjectSessionDiscovery | null> {
|
||||||
|
const cacheKey = buildProjectSessionDiscoveryCacheKey(teamName, options);
|
||||||
const context = await this.projectResolver.getContext(teamName, options);
|
const context = await this.projectResolver.getContext(teamName, options);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
logger.debug(`No transcript context for team "${teamName}"`);
|
logger.debug(`No transcript context for team "${teamName}"`);
|
||||||
|
|
@ -1214,8 +1252,8 @@ export class TeamMemberLogsFinder {
|
||||||
}
|
}
|
||||||
|
|
||||||
const discovery = { projectDir, projectId, config, sessionIds, knownMembers };
|
const discovery = { projectDir, projectId, config, sessionIds, knownMembers };
|
||||||
if ((this.discoveryGenerationByTeam.get(teamName) ?? 0) === generation) {
|
if ((this.discoveryGenerationByTeam.get(cacheKey) ?? 0) === generation) {
|
||||||
this.discoveryCache.set(teamName, {
|
this.discoveryCache.set(cacheKey, {
|
||||||
result: discovery,
|
result: discovery,
|
||||||
expiresAt: Date.now() + DISCOVERY_CACHE_TTL,
|
expiresAt: Date.now() + DISCOVERY_CACHE_TTL,
|
||||||
});
|
});
|
||||||
|
|
@ -1585,7 +1623,15 @@ export class TeamMemberLogsFinder {
|
||||||
if (this.attributionCache.has(cacheKey)) {
|
if (this.attributionCache.has(cacheKey)) {
|
||||||
return this.attributionCache.get(cacheKey) ?? null;
|
return this.attributionCache.get(cacheKey) ?? null;
|
||||||
}
|
}
|
||||||
const attribution = await this.attributeSubagent(filePath, knownMembers);
|
const existing = this.attributionInFlight.get(cacheKey);
|
||||||
|
if (existing) {
|
||||||
|
return (await existing) as SubagentAttribution | null;
|
||||||
|
}
|
||||||
|
const promise = this.attributeSubagent(filePath, knownMembers).finally(() => {
|
||||||
|
this.attributionInFlight.delete(cacheKey);
|
||||||
|
});
|
||||||
|
this.attributionInFlight.set(cacheKey, promise);
|
||||||
|
const attribution = await promise;
|
||||||
this.attributionCache.set(cacheKey, attribution);
|
this.attributionCache.set(cacheKey, attribution);
|
||||||
if (this.attributionCache.size > ATTRIBUTION_CACHE_MAX) {
|
if (this.attributionCache.size > ATTRIBUTION_CACHE_MAX) {
|
||||||
const oldestKey = this.attributionCache.keys().next().value;
|
const oldestKey = this.attributionCache.keys().next().value;
|
||||||
|
|
@ -1604,7 +1650,15 @@ export class TeamMemberLogsFinder {
|
||||||
if (this.attributionCache.has(cacheKey)) {
|
if (this.attributionCache.has(cacheKey)) {
|
||||||
return (this.attributionCache.get(cacheKey) as RootSessionAttribution | null) ?? null;
|
return (this.attributionCache.get(cacheKey) as RootSessionAttribution | null) ?? null;
|
||||||
}
|
}
|
||||||
const attribution = await this.attributeMemberSession(filePath, teamName, knownMembers);
|
const existing = this.attributionInFlight.get(cacheKey);
|
||||||
|
if (existing) {
|
||||||
|
return (await existing) as RootSessionAttribution | null;
|
||||||
|
}
|
||||||
|
const promise = this.attributeMemberSession(filePath, teamName, knownMembers).finally(() => {
|
||||||
|
this.attributionInFlight.delete(cacheKey);
|
||||||
|
});
|
||||||
|
this.attributionInFlight.set(cacheKey, promise);
|
||||||
|
const attribution = await promise;
|
||||||
this.attributionCache.set(cacheKey, attribution);
|
this.attributionCache.set(cacheKey, attribution);
|
||||||
if (this.attributionCache.size > ATTRIBUTION_CACHE_MAX) {
|
if (this.attributionCache.size > ATTRIBUTION_CACHE_MAX) {
|
||||||
const oldestKey = this.attributionCache.keys().next().value;
|
const oldestKey = this.attributionCache.keys().next().value;
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,10 @@ import {
|
||||||
import { TeamConfigReader } from './TeamConfigReader';
|
import { TeamConfigReader } from './TeamConfigReader';
|
||||||
import { TeamInboxReader } from './TeamInboxReader';
|
import { TeamInboxReader } from './TeamInboxReader';
|
||||||
import { TeamInboxWriter } from './TeamInboxWriter';
|
import { TeamInboxWriter } from './TeamInboxWriter';
|
||||||
import { writeTeamLaunchFailureArtifactPack } from './TeamLaunchFailureArtifactPack';
|
import {
|
||||||
|
isWorkspaceTrustLaunchFailureText,
|
||||||
|
writeTeamLaunchFailureArtifactPack,
|
||||||
|
} from './TeamLaunchFailureArtifactPack';
|
||||||
import {
|
import {
|
||||||
createPersistedLaunchSnapshot,
|
createPersistedLaunchSnapshot,
|
||||||
deriveTeamLaunchAggregateState,
|
deriveTeamLaunchAggregateState,
|
||||||
|
|
@ -924,6 +927,12 @@ function classifyDeterministicBootstrapFailure(reason: string): {
|
||||||
} {
|
} {
|
||||||
const normalizedReason = reason.trim();
|
const normalizedReason = reason.trim();
|
||||||
const lower = normalizedReason.toLowerCase();
|
const lower = normalizedReason.toLowerCase();
|
||||||
|
if (isWorkspaceTrustLaunchFailureText(normalizedReason)) {
|
||||||
|
return {
|
||||||
|
title: 'Workspace trust required',
|
||||||
|
normalizedReason,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (lower.includes('disabled by kill switch')) {
|
if (lower.includes('disabled by kill switch')) {
|
||||||
return {
|
return {
|
||||||
title: 'Deterministic bootstrap disabled',
|
title: 'Deterministic bootstrap disabled',
|
||||||
|
|
@ -1860,6 +1869,8 @@ interface ProvisioningRun {
|
||||||
lastMemberSpawnAuditConfigReadWarningAt: number;
|
lastMemberSpawnAuditConfigReadWarningAt: number;
|
||||||
/** Per-member warning throttle for repeated "missing from config" logs. */
|
/** Per-member warning throttle for repeated "missing from config" logs. */
|
||||||
lastMemberSpawnAuditMissingWarningAt: Map<string, number>;
|
lastMemberSpawnAuditMissingWarningAt: Map<string, number>;
|
||||||
|
/** Prevents duplicate Team Launched notifications for the same live run. */
|
||||||
|
teamLaunchedNotificationFired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROVISIONING_TRACE_STORAGE_LIMIT = 500;
|
const PROVISIONING_TRACE_STORAGE_LIMIT = 500;
|
||||||
|
|
@ -11825,12 +11836,7 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
this.invalidateRuntimeSnapshotCaches(input.teamName);
|
this.invalidateRuntimeSnapshotCaches(input.teamName);
|
||||||
if (trackedUpdate.changed) {
|
if (trackedUpdate.changed) {
|
||||||
this.teamChangeEmitter?.({
|
this.emitMemberSpawnChange(trackedUpdate.run, input.memberName);
|
||||||
type: 'member-spawn',
|
|
||||||
teamName: input.teamName,
|
|
||||||
runId: input.runId,
|
|
||||||
detail: input.memberName,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -24788,7 +24794,7 @@ export class TeamProvisioningService {
|
||||||
private emitMemberSpawnChange(
|
private emitMemberSpawnChange(
|
||||||
run: Pick<ProvisioningRun, 'teamName' | 'runId'>,
|
run: Pick<ProvisioningRun, 'teamName' | 'runId'>,
|
||||||
memberName: string
|
memberName: string
|
||||||
) {
|
): void {
|
||||||
this.invalidateMemberSpawnStatusesCache(run.teamName);
|
this.invalidateMemberSpawnStatusesCache(run.teamName);
|
||||||
this.teamChangeEmitter?.({
|
this.teamChangeEmitter?.({
|
||||||
type: 'member-spawn',
|
type: 'member-spawn',
|
||||||
|
|
@ -24796,6 +24802,39 @@ export class TeamProvisioningService {
|
||||||
runId: run.runId,
|
runId: run.runId,
|
||||||
detail: memberName,
|
detail: memberName,
|
||||||
});
|
});
|
||||||
|
const trackedRun = this.runs.get(run.runId);
|
||||||
|
if (trackedRun?.teamName === run.teamName) {
|
||||||
|
void this.maybeFireTeamLaunchedNotificationWhenAllMembersJoined(trackedRun);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async maybeFireTeamLaunchedNotificationWhenAllMembersJoined(
|
||||||
|
run: ProvisioningRun
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
!run.isLaunch ||
|
||||||
|
run.teamLaunchedNotificationFired ||
|
||||||
|
run.processKilled ||
|
||||||
|
run.cancelRequested ||
|
||||||
|
!this.isProvisioningRunPromotedToAlive(run) ||
|
||||||
|
!this.areAllExpectedLaunchMembersConfirmed(run)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fireTeamLaunchedNotification(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
private areAllExpectedLaunchMembersConfirmed(run: ProvisioningRun): boolean {
|
||||||
|
const expectedMembers = run.expectedMembers ?? [];
|
||||||
|
if (expectedMembers.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectedMembers.every((memberName) => {
|
||||||
|
const member = run.memberSpawnStatuses.get(memberName);
|
||||||
|
return member?.launchState === 'confirmed_alive' || member?.bootstrapConfirmed === true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async publishMixedSecondaryLaneStatusChange(
|
private async publishMixedSecondaryLaneStatusChange(
|
||||||
|
|
@ -30499,12 +30538,21 @@ export class TeamProvisioningService {
|
||||||
* Uses the existing addTeamNotification() pipeline.
|
* Uses the existing addTeamNotification() pipeline.
|
||||||
*/
|
*/
|
||||||
private async fireTeamLaunchedNotification(run: ProvisioningRun): Promise<void> {
|
private async fireTeamLaunchedNotification(run: ProvisioningRun): Promise<void> {
|
||||||
|
if (run.teamLaunchedNotificationFired) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
run.teamLaunchedNotificationFired = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = ConfigManager.getInstance().getConfig();
|
const config = ConfigManager.getInstance().getConfig();
|
||||||
const suppressToast = !config.notifications.notifyOnTeamLaunched;
|
const suppressToast = !config.notifications.notifyOnTeamLaunched;
|
||||||
const displayName = run.request.displayName || run.teamName;
|
const displayName = run.request.displayName || run.teamName;
|
||||||
|
const joinedCount = run.expectedMembers?.length ?? 0;
|
||||||
|
const allJoined = joinedCount > 0 && this.areAllExpectedLaunchMembersConfirmed(run);
|
||||||
const body = run.isLaunch
|
const body = run.isLaunch
|
||||||
? `Team "${displayName}" has been launched and is ready for tasks.`
|
? allJoined
|
||||||
|
? `Team "${displayName}" has been launched - all ${joinedCount} teammates joined and are ready for tasks.`
|
||||||
|
: `Team "${displayName}" has been launched and is ready for tasks.`
|
||||||
: `Team "${displayName}" has been provisioned and is ready for tasks.`;
|
: `Team "${displayName}" has been provisioned and is ready for tasks.`;
|
||||||
|
|
||||||
await NotificationManager.getInstance().addTeamNotification({
|
await NotificationManager.getInstance().addTeamNotification({
|
||||||
|
|
@ -30520,6 +30568,7 @@ export class TeamProvisioningService {
|
||||||
suppressToast,
|
suppressToast,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
run.teamLaunchedNotificationFired = false;
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[${run.teamName}] Failed to fire team_launched notification: ${
|
`[${run.teamName}] Failed to fire team_launched notification: ${
|
||||||
error instanceof Error ? error.message : String(error)
|
error instanceof Error ? error.message : String(error)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const logger = createLogger('Service:TeamTranscriptProjectResolver');
|
||||||
const SESSION_DISCOVERY_CACHE_TTL = 30_000;
|
const SESSION_DISCOVERY_CACHE_TTL = 30_000;
|
||||||
const TEAM_AFFINITY_SCAN_LINES = 40;
|
const TEAM_AFFINITY_SCAN_LINES = 40;
|
||||||
const ROOT_DISCOVERY_CONCURRENCY = 12;
|
const ROOT_DISCOVERY_CONCURRENCY = 12;
|
||||||
|
const FAST_CONTEXT_ROOT_DISCOVERY_MTIME_GRACE_MS = 24 * 60 * 60_000;
|
||||||
|
|
||||||
type ProjectEvidenceSource =
|
type ProjectEvidenceSource =
|
||||||
| 'projectPath'
|
| 'projectPath'
|
||||||
|
|
@ -51,6 +52,11 @@ interface TeamTranscriptProjectConfigReader {
|
||||||
getConfigSnapshot?: (teamName: string) => Promise<TeamConfig | null>;
|
getConfigSnapshot?: (teamName: string) => Promise<TeamConfig | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TeamTranscriptProjectContextOptions {
|
||||||
|
forceRefresh?: boolean;
|
||||||
|
includeTeamSubagentSessionDiscovery?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
type ScannedSessionProjectMatch = Omit<SessionProjectMatch, 'projectPath'> & {
|
type ScannedSessionProjectMatch = Omit<SessionProjectMatch, 'projectPath'> & {
|
||||||
projectPath?: string;
|
projectPath?: string;
|
||||||
};
|
};
|
||||||
|
|
@ -72,6 +78,45 @@ function isSessionDirectoryName(name: string): boolean {
|
||||||
return name !== 'memory' && !name.startsWith('.');
|
return name !== 'memory' && !name.startsWith('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildContextCacheKey(
|
||||||
|
teamName: string,
|
||||||
|
options?: TeamTranscriptProjectContextOptions
|
||||||
|
): string {
|
||||||
|
const subagentMode = options?.includeTeamSubagentSessionDiscovery === false ? 'known' : 'full';
|
||||||
|
return `${teamName}\0${subagentMode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTimestampMs(value: unknown): number | null {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string' && value.trim().length > 0) {
|
||||||
|
const parsed = Date.parse(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function teamLifecycleMtimeCutoffMs(config: TeamConfig): number | null {
|
||||||
|
const timestamps: number[] = [];
|
||||||
|
const createdAt = parseTimestampMs((config as { createdAt?: unknown }).createdAt);
|
||||||
|
if (createdAt !== null) {
|
||||||
|
timestamps.push(createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const member of config.members ?? []) {
|
||||||
|
const joinedAt = parseTimestampMs((member as { joinedAt?: unknown }).joinedAt);
|
||||||
|
if (joinedAt !== null) {
|
||||||
|
timestamps.push(joinedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestamps.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.min(...timestamps) - FAST_CONTEXT_ROOT_DISCOVERY_MTIME_GRACE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeProjectPathCandidate(value: unknown): string | null {
|
function normalizeProjectPathCandidate(value: unknown): string | null {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -207,12 +252,21 @@ export class TeamTranscriptProjectResolver {
|
||||||
: this.configReader.getConfig(teamName);
|
: this.configReader.getConfig(teamName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private deleteContextCacheForTeam(teamName: string): void {
|
||||||
|
this.contextCache.delete(teamName);
|
||||||
|
for (const key of this.contextCache.keys()) {
|
||||||
|
if (key === teamName || key.startsWith(`${teamName}\0`)) {
|
||||||
|
this.contextCache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getLiveBaseContext(
|
async getLiveBaseContext(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
options?: { forceRefresh?: boolean; extraProjectPathCandidates?: readonly unknown[] }
|
options?: { forceRefresh?: boolean; extraProjectPathCandidates?: readonly unknown[] }
|
||||||
): Promise<TeamTranscriptProjectLiveBaseContext | null> {
|
): Promise<TeamTranscriptProjectLiveBaseContext | null> {
|
||||||
if (options?.forceRefresh) {
|
if (options?.forceRefresh) {
|
||||||
this.contextCache.delete(teamName);
|
this.deleteContextCacheForTeam(teamName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await this.readConfigForObservation(teamName);
|
const config = await this.readConfigForObservation(teamName);
|
||||||
|
|
@ -244,13 +298,14 @@ export class TeamTranscriptProjectResolver {
|
||||||
|
|
||||||
async getContext(
|
async getContext(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
options?: { forceRefresh?: boolean }
|
options?: TeamTranscriptProjectContextOptions
|
||||||
): Promise<TeamTranscriptProjectContext | null> {
|
): Promise<TeamTranscriptProjectContext | null> {
|
||||||
|
const cacheKey = buildContextCacheKey(teamName, options);
|
||||||
if (options?.forceRefresh) {
|
if (options?.forceRefresh) {
|
||||||
this.contextCache.delete(teamName);
|
this.deleteContextCacheForTeam(teamName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = this.contextCache.get(teamName);
|
const cached = this.contextCache.get(cacheKey);
|
||||||
if (cached && cached.expiresAt > Date.now()) {
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
return cached.value;
|
return cached.value;
|
||||||
}
|
}
|
||||||
|
|
@ -282,7 +337,8 @@ export class TeamTranscriptProjectResolver {
|
||||||
const sessionIds = await this.discoverSessionIds(
|
const sessionIds = await this.discoverSessionIds(
|
||||||
teamName,
|
teamName,
|
||||||
resolution.projectDir,
|
resolution.projectDir,
|
||||||
resolvedConfig
|
resolvedConfig,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
const value = {
|
const value = {
|
||||||
projectDir: resolution.projectDir,
|
projectDir: resolution.projectDir,
|
||||||
|
|
@ -290,7 +346,7 @@ export class TeamTranscriptProjectResolver {
|
||||||
config: resolvedConfig,
|
config: resolvedConfig,
|
||||||
sessionIds,
|
sessionIds,
|
||||||
};
|
};
|
||||||
this.contextCache.set(teamName, {
|
this.contextCache.set(cacheKey, {
|
||||||
value,
|
value,
|
||||||
expiresAt: Date.now() + SESSION_DISCOVERY_CACHE_TTL,
|
expiresAt: Date.now() + SESSION_DISCOVERY_CACHE_TTL,
|
||||||
});
|
});
|
||||||
|
|
@ -741,12 +797,20 @@ export class TeamTranscriptProjectResolver {
|
||||||
private async discoverSessionIds(
|
private async discoverSessionIds(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
config: TeamConfig
|
config: TeamConfig,
|
||||||
|
options?: TeamTranscriptProjectContextOptions
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const knownSessionIds = collectKnownSessionIds(config);
|
const knownSessionIds = collectKnownSessionIds(config);
|
||||||
const [teamRootSessionIds, sessionDirIds] = await Promise.all([
|
const includeTeamSubagentSessionDiscovery =
|
||||||
this.listTeamRootSessionIds(projectDir, teamName),
|
options?.includeTeamSubagentSessionDiscovery !== false;
|
||||||
this.listSessionDirIds(projectDir),
|
const rootMtimeSinceMs = includeTeamSubagentSessionDiscovery
|
||||||
|
? null
|
||||||
|
: teamLifecycleMtimeCutoffMs(config);
|
||||||
|
const [teamRootSessionIds, teamSubagentSessionIds] = await Promise.all([
|
||||||
|
this.listTeamRootSessionIds(projectDir, teamName, rootMtimeSinceMs),
|
||||||
|
includeTeamSubagentSessionDiscovery
|
||||||
|
? this.listTeamSubagentSessionIds(projectDir, teamName)
|
||||||
|
: Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const orderedSessionIds: string[] = [];
|
const orderedSessionIds: string[] = [];
|
||||||
|
|
@ -762,7 +826,7 @@ export class TeamTranscriptProjectResolver {
|
||||||
for (const sessionId of knownSessionIds) {
|
for (const sessionId of knownSessionIds) {
|
||||||
push(sessionId);
|
push(sessionId);
|
||||||
}
|
}
|
||||||
for (const sessionId of [...teamRootSessionIds, ...sessionDirIds].sort((left, right) =>
|
for (const sessionId of [...teamRootSessionIds, ...teamSubagentSessionIds].sort((left, right) =>
|
||||||
left.localeCompare(right)
|
left.localeCompare(right)
|
||||||
)) {
|
)) {
|
||||||
push(sessionId);
|
push(sessionId);
|
||||||
|
|
@ -816,21 +880,69 @@ export class TeamTranscriptProjectResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async listSessionDirIds(projectDir: string): Promise<string[]> {
|
private async listTeamSubagentSessionIds(
|
||||||
|
projectDir: string,
|
||||||
|
teamName: string
|
||||||
|
): Promise<string[]> {
|
||||||
const dirEntries = await this.readProjectDirEntries(projectDir);
|
const dirEntries = await this.readProjectDirEntries(projectDir);
|
||||||
if (!dirEntries) {
|
if (!dirEntries) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return dirEntries
|
const sessionDirEntries = dirEntries.filter(
|
||||||
.filter((entry) => entry.isDirectory() && isSessionDirectoryName(entry.name))
|
(entry) => entry.isDirectory() && isSessionDirectoryName(entry.name)
|
||||||
.map((entry) => entry.name);
|
);
|
||||||
|
const discovered = new Set<string>();
|
||||||
|
let nextIndex = 0;
|
||||||
|
|
||||||
|
const scanNextSessionDir = async (): Promise<void> => {
|
||||||
|
while (nextIndex < sessionDirEntries.length) {
|
||||||
|
const entry = sessionDirEntries[nextIndex++];
|
||||||
|
const subagentsDir = path.join(projectDir, entry.name, 'subagents');
|
||||||
|
let subagentEntries: Dirent[];
|
||||||
|
try {
|
||||||
|
subagentEntries = await fs.readdir(subagentsDir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const subagentEntry of subagentEntries) {
|
||||||
|
if (!subagentEntry.isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!subagentEntry.name.endsWith('.jsonl')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!subagentEntry.name.startsWith('agent-')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (subagentEntry.name.startsWith('agent-acompact')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(subagentsDir, subagentEntry.name);
|
||||||
|
if (await this.fileBelongsToTeam(filePath, teamName)) {
|
||||||
|
discovered.add(entry.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from({ length: Math.min(ROOT_DISCOVERY_CONCURRENCY, sessionDirEntries.length) }, () =>
|
||||||
|
scanNextSessionDir()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...discovered];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectRootJsonlSessionIds(
|
private async collectRootJsonlSessionIds(
|
||||||
rootJsonlEntries: Dirent[],
|
rootJsonlEntries: Dirent[],
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
teamName: string
|
teamName: string,
|
||||||
|
mtimeSinceMs?: number | null
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const discovered = new Set<string>();
|
const discovered = new Set<string>();
|
||||||
let nextIndex = 0;
|
let nextIndex = 0;
|
||||||
|
|
@ -839,6 +951,16 @@ export class TeamTranscriptProjectResolver {
|
||||||
while (nextIndex < rootJsonlEntries.length) {
|
while (nextIndex < rootJsonlEntries.length) {
|
||||||
const entry = rootJsonlEntries[nextIndex++];
|
const entry = rootJsonlEntries[nextIndex++];
|
||||||
const filePath = path.join(projectDir, entry.name);
|
const filePath = path.join(projectDir, entry.name);
|
||||||
|
if (mtimeSinceMs != null) {
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(filePath);
|
||||||
|
if (!stat.isFile() || stat.mtimeMs < mtimeSinceMs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!(await this.fileBelongsToTeam(filePath, teamName))) {
|
if (!(await this.fileBelongsToTeam(filePath, teamName))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -855,7 +977,11 @@ export class TeamTranscriptProjectResolver {
|
||||||
return [...discovered];
|
return [...discovered];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async listTeamRootSessionIds(projectDir: string, teamName: string): Promise<string[]> {
|
private async listTeamRootSessionIds(
|
||||||
|
projectDir: string,
|
||||||
|
teamName: string,
|
||||||
|
mtimeSinceMs?: number | null
|
||||||
|
): Promise<string[]> {
|
||||||
const dirEntries = await this.readProjectDirEntries(projectDir);
|
const dirEntries = await this.readProjectDirEntries(projectDir);
|
||||||
if (!dirEntries) {
|
if (!dirEntries) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -864,7 +990,7 @@ export class TeamTranscriptProjectResolver {
|
||||||
const rootJsonlEntries = dirEntries.filter(
|
const rootJsonlEntries = dirEntries.filter(
|
||||||
(entry) => entry.isFile() && entry.name.endsWith('.jsonl')
|
(entry) => entry.isFile() && entry.name.endsWith('.jsonl')
|
||||||
);
|
);
|
||||||
return this.collectRootJsonlSessionIds(rootJsonlEntries, projectDir, teamName);
|
return this.collectRootJsonlSessionIds(rootJsonlEntries, projectDir, teamName, mtimeSinceMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fileBelongsToTeam(filePath: string, teamName: string): Promise<boolean> {
|
private async fileBelongsToTeam(filePath: string, teamName: string): Promise<boolean> {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
import type { OpenCodeTeamRuntimeBridgePort } from '../../runtime/OpenCodeTeamRuntimeAdapter';
|
|
||||||
import { stableHash } from './OpenCodeBridgeCommandContract';
|
import { stableHash } from './OpenCodeBridgeCommandContract';
|
||||||
|
|
||||||
|
import type { OpenCodeTeamRuntimeBridgePort } from '../../runtime/OpenCodeTeamRuntimeAdapter';
|
||||||
import type {
|
import type {
|
||||||
OpenCodeTeamLaunchReadiness,
|
OpenCodeTeamLaunchReadiness,
|
||||||
OpenCodeTeamLaunchReadinessState,
|
OpenCodeTeamLaunchReadinessState,
|
||||||
|
|
@ -14,10 +15,10 @@ import type {
|
||||||
OpenCodeBridgeFailureKind,
|
OpenCodeBridgeFailureKind,
|
||||||
OpenCodeBridgeResult,
|
OpenCodeBridgeResult,
|
||||||
OpenCodeBridgeRuntimeSnapshot,
|
OpenCodeBridgeRuntimeSnapshot,
|
||||||
OpenCodeCommandStatusCommandBody,
|
|
||||||
OpenCodeCommandStatusCommandData,
|
|
||||||
OpenCodeCleanupHostsCommandBody,
|
OpenCodeCleanupHostsCommandBody,
|
||||||
OpenCodeCleanupHostsCommandData,
|
OpenCodeCleanupHostsCommandData,
|
||||||
|
OpenCodeCommandStatusCommandBody,
|
||||||
|
OpenCodeCommandStatusCommandData,
|
||||||
OpenCodeLaunchTeamCommandBody,
|
OpenCodeLaunchTeamCommandBody,
|
||||||
OpenCodeLaunchTeamCommandData,
|
OpenCodeLaunchTeamCommandData,
|
||||||
OpenCodeObserveMessageDeliveryCommandBody,
|
OpenCodeObserveMessageDeliveryCommandBody,
|
||||||
|
|
@ -79,10 +80,6 @@ const DEFAULT_CLEANUP_TIMEOUT_MS = 10_000;
|
||||||
const DEFAULT_BACKFILL_TIMEOUT_MS = 45_000;
|
const DEFAULT_BACKFILL_TIMEOUT_MS = 45_000;
|
||||||
const DEFAULT_COMMAND_STATUS_TIMEOUT_MS = 5_000;
|
const DEFAULT_COMMAND_STATUS_TIMEOUT_MS = 5_000;
|
||||||
|
|
||||||
function isCommandStatusRecoveryEnabled(): boolean {
|
|
||||||
return process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY === '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSendPayloadHash(input: OpenCodeSendMessageCommandBody): string {
|
function buildSendPayloadHash(input: OpenCodeSendMessageCommandBody): string {
|
||||||
const { payloadHash: _payloadHash, ...hashable } = input;
|
const { payloadHash: _payloadHash, ...hashable } = input;
|
||||||
return stableHash(hashable);
|
return stableHash(hashable);
|
||||||
|
|
@ -246,7 +243,7 @@ export class OpenCodeReadinessBridge implements OpenCodeTeamRuntimeBridgePort {
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
if (result.error.kind === 'timeout' && isCommandStatusRecoveryEnabled()) {
|
if (result.error.kind === 'timeout') {
|
||||||
const recovered = await this.recoverTimedOutSendMessage({
|
const recovered = await this.recoverTimedOutSendMessage({
|
||||||
originalRequestId: commandRequestId,
|
originalRequestId: commandRequestId,
|
||||||
body,
|
body,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
import { classifyRuntimeDiagnostic } from '../../runtime/RuntimeDiagnosticClassifier';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isActionRequiredOpenCodeRuntimeDeliveryReason,
|
isActionRequiredOpenCodeRuntimeDeliveryReason,
|
||||||
selectOpenCodeRuntimeDeliveryReason,
|
selectOpenCodeRuntimeDeliveryReason,
|
||||||
} from './OpenCodeRuntimeDeliveryDiagnostics';
|
} from './OpenCodeRuntimeDeliveryDiagnostics';
|
||||||
import { classifyRuntimeDiagnostic } from '../../runtime/RuntimeDiagnosticClassifier';
|
|
||||||
|
|
||||||
import type { OpenCodePromptDeliveryLedgerRecord } from './OpenCodePromptDeliveryLedger';
|
import type { OpenCodePromptDeliveryLedgerRecord } from './OpenCodePromptDeliveryLedger';
|
||||||
import type {
|
import type {
|
||||||
|
|
|
||||||
|
|
@ -621,6 +621,13 @@ function getOpenCodeInstallLabel(status: OpenCodeRuntimeStatus | null): string {
|
||||||
return 'Install';
|
return 'Install';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OPENCODE_PROVIDER_FREE_BADGE_TITLE =
|
||||||
|
'OpenCode includes free model options such as Big Pickle when available in your setup. OpenRouter through OpenCode can also expose free models, but not every OpenCode/OpenRouter model is free. Availability and limits may change.';
|
||||||
|
|
||||||
|
function shouldShowOpenCodeProviderFreeBadge(provider: CliProviderStatus): boolean {
|
||||||
|
return provider.providerId === 'opencode';
|
||||||
|
}
|
||||||
|
|
||||||
const InstalledBanner = ({
|
const InstalledBanner = ({
|
||||||
cliStatus,
|
cliStatus,
|
||||||
sourceProviderMap,
|
sourceProviderMap,
|
||||||
|
|
@ -847,6 +854,14 @@ const InstalledBanner = ({
|
||||||
? getProviderLabel(provider.providerId)
|
? getProviderLabel(provider.providerId)
|
||||||
: provider.displayName}
|
: provider.displayName}
|
||||||
</span>
|
</span>
|
||||||
|
{shouldShowOpenCodeProviderFreeBadge(provider) ? (
|
||||||
|
<span
|
||||||
|
className="rounded bg-[rgba(34,197,94,0.14)] px-1.5 py-px text-[9px] font-medium uppercase tracking-[0.06em] text-[rgb(74,222,128)]"
|
||||||
|
title={OPENCODE_PROVIDER_FREE_BADGE_TITLE}
|
||||||
|
>
|
||||||
|
Free models
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
|
|
@ -1039,6 +1054,7 @@ const InstalledBanner = ({
|
||||||
modelAvailability={provider.modelAvailability}
|
modelAvailability={provider.modelAvailability}
|
||||||
providerStatus={provider}
|
providerStatus={provider}
|
||||||
collapseAfter={15}
|
collapseAfter={15}
|
||||||
|
maxCollapsedRows={provider.providerId === 'opencode' ? 2 : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -270,11 +270,6 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
||||||
void mcpFetchInstalled(projectPath ?? undefined);
|
void mcpFetchInstalled(projectPath ?? undefined);
|
||||||
}, [mcpFetchInstalled, projectPath]);
|
}, [mcpFetchInstalled, projectPath]);
|
||||||
|
|
||||||
// Fetch API keys on mount
|
|
||||||
useEffect(() => {
|
|
||||||
void fetchApiKeys();
|
|
||||||
}, [fetchApiKeys]);
|
|
||||||
|
|
||||||
// Fetch Skills catalog on mount / project change
|
// Fetch Skills catalog on mount / project change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchSkillsCatalog(projectPath ?? undefined);
|
void fetchSkillsCatalog(projectPath ?? undefined);
|
||||||
|
|
@ -287,7 +282,9 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
||||||
bootstrapCliStatus,
|
bootstrapCliStatus,
|
||||||
fetchCliStatus,
|
fetchCliStatus,
|
||||||
});
|
});
|
||||||
|
if (tabState.activeSubTab === 'api-keys') {
|
||||||
void fetchApiKeys();
|
void fetchApiKeys();
|
||||||
|
}
|
||||||
void fetchPluginCatalog(projectPath ?? undefined, true);
|
void fetchPluginCatalog(projectPath ?? undefined, true);
|
||||||
void mcpBrowse(); // re-fetch first page
|
void mcpBrowse(); // re-fetch first page
|
||||||
void mcpFetchInstalled(projectPath ?? undefined);
|
void mcpFetchInstalled(projectPath ?? undefined);
|
||||||
|
|
@ -302,6 +299,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
||||||
mcpBrowse,
|
mcpBrowse,
|
||||||
mcpFetchInstalled,
|
mcpFetchInstalled,
|
||||||
projectPath,
|
projectPath,
|
||||||
|
tabState.activeSubTab,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isRefreshing =
|
const isRefreshing =
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export const ApiKeysPanel = ({
|
||||||
apiKeysLoading,
|
apiKeysLoading,
|
||||||
apiKeysError,
|
apiKeysError,
|
||||||
storageStatus,
|
storageStatus,
|
||||||
|
fetchApiKeys,
|
||||||
fetchStorageStatus,
|
fetchStorageStatus,
|
||||||
cliStatus,
|
cliStatus,
|
||||||
cliStatusLoading,
|
cliStatusLoading,
|
||||||
|
|
@ -46,6 +47,7 @@ export const ApiKeysPanel = ({
|
||||||
apiKeysLoading: s.apiKeysLoading,
|
apiKeysLoading: s.apiKeysLoading,
|
||||||
apiKeysError: s.apiKeysError,
|
apiKeysError: s.apiKeysError,
|
||||||
storageStatus: s.apiKeyStorageStatus,
|
storageStatus: s.apiKeyStorageStatus,
|
||||||
|
fetchApiKeys: s.fetchApiKeys,
|
||||||
fetchStorageStatus: s.fetchApiKeyStorageStatus,
|
fetchStorageStatus: s.fetchApiKeyStorageStatus,
|
||||||
cliStatus: s.cliStatus,
|
cliStatus: s.cliStatus,
|
||||||
cliStatusLoading: s.cliStatusLoading,
|
cliStatusLoading: s.cliStatusLoading,
|
||||||
|
|
@ -85,8 +87,9 @@ export const ApiKeysPanel = ({
|
||||||
const [editingKey, setEditingKey] = useState<ApiKeyEntry | null>(null);
|
const [editingKey, setEditingKey] = useState<ApiKeyEntry | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
void fetchApiKeys();
|
||||||
void fetchStorageStatus();
|
void fetchStorageStatus();
|
||||||
}, [fetchStorageStatus]);
|
}, [fetchApiKeys, fetchStorageStatus]);
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
setEditingKey(null);
|
setEditingKey(null);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { cn } from '@renderer/lib/utils';
|
import { cn } from '@renderer/lib/utils';
|
||||||
import {
|
import {
|
||||||
|
|
@ -56,13 +56,43 @@ function getCatalogBadgeLabel(
|
||||||
return catalogItem?.badgeLabel?.trim() || null;
|
return catalogItem?.badgeLabel?.trim() || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeBadgeText(value: string): string {
|
||||||
|
return value.trim().replace(/\s+/g, ' ').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRenderCatalogBadge(modelLabel: string, catalogBadgeLabel: string | null): boolean {
|
||||||
|
if (!catalogBadgeLabel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return normalizeBadgeText(modelLabel) !== normalizeBadgeText(catalogBadgeLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasChildAfterRowLimit(container: HTMLElement, rowLimit: number): boolean {
|
||||||
|
const rowTops: number[] = [];
|
||||||
|
const children = Array.from(container.children) as HTMLElement[];
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
const top = child.offsetTop;
|
||||||
|
let rowIndex = rowTops.findIndex((rowTop) => Math.abs(rowTop - top) <= 1);
|
||||||
|
if (rowIndex < 0) {
|
||||||
|
rowTops.push(top);
|
||||||
|
rowIndex = rowTops.length - 1;
|
||||||
|
}
|
||||||
|
if (rowIndex >= rowLimit) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export const ProviderModelBadges = ({
|
export const ProviderModelBadges = ({
|
||||||
providerId,
|
providerId,
|
||||||
models,
|
models,
|
||||||
modelAvailability,
|
modelAvailability,
|
||||||
providerStatus,
|
providerStatus,
|
||||||
collapseAfter,
|
collapseAfter,
|
||||||
expandedMaxHeightPx = 200,
|
maxCollapsedRows,
|
||||||
}: {
|
}: {
|
||||||
readonly providerId: CliProviderId;
|
readonly providerId: CliProviderId;
|
||||||
readonly models: string[];
|
readonly models: string[];
|
||||||
|
|
@ -72,16 +102,73 @@ export const ProviderModelBadges = ({
|
||||||
'providerId' | 'authMethod' | 'backend' | 'modelCatalog'
|
'providerId' | 'authMethod' | 'backend' | 'modelCatalog'
|
||||||
> | null;
|
> | null;
|
||||||
readonly collapseAfter?: number;
|
readonly collapseAfter?: number;
|
||||||
readonly expandedMaxHeightPx?: number;
|
readonly maxCollapsedRows?: number;
|
||||||
}): React.JSX.Element => {
|
}): React.JSX.Element => {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [collapsedModelLimit, setCollapsedModelLimit] = useState<number | null>(null);
|
||||||
|
const [measureTick, setMeasureTick] = useState(0);
|
||||||
|
const listRef = useRef<HTMLDivElement | null>(null);
|
||||||
const visibleModels = getVisibleTeamProviderModels(providerId, models, providerStatus);
|
const visibleModels = getVisibleTeamProviderModels(providerId, models, providerStatus);
|
||||||
const displayModelAvailability = providerId === 'opencode' ? undefined : modelAvailability;
|
const displayModelAvailability = providerId === 'opencode' ? undefined : modelAvailability;
|
||||||
const shouldCollapse =
|
const shouldCollapse =
|
||||||
typeof collapseAfter === 'number' && collapseAfter > 0 && visibleModels.length > collapseAfter;
|
typeof collapseAfter === 'number' && collapseAfter > 0 && visibleModels.length > collapseAfter;
|
||||||
|
const collapsedBaseLimit = shouldCollapse ? collapseAfter : visibleModels.length;
|
||||||
|
const collapsedLimit =
|
||||||
|
shouldCollapse && !expanded
|
||||||
|
? Math.max(0, Math.min(collapsedModelLimit ?? collapsedBaseLimit, collapsedBaseLimit))
|
||||||
|
: visibleModels.length;
|
||||||
const displayedModels =
|
const displayedModels =
|
||||||
shouldCollapse && !expanded ? visibleModels.slice(0, collapseAfter) : visibleModels;
|
shouldCollapse && !expanded ? visibleModels.slice(0, collapsedLimit) : visibleModels;
|
||||||
const hiddenCount = shouldCollapse ? visibleModels.length - collapseAfter : 0;
|
const hiddenCount = shouldCollapse ? visibleModels.length - displayedModels.length : 0;
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setCollapsedModelLimit(null);
|
||||||
|
}, [collapseAfter, maxCollapsedRows, models, providerStatus]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!shouldCollapse || expanded || !maxCollapsedRows || maxCollapsedRows < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = listRef.current;
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChildAfterRowLimit(container, maxCollapsedRows)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextLimit = Math.max(0, collapsedLimit - 1);
|
||||||
|
if (nextLimit !== collapsedLimit) {
|
||||||
|
setCollapsedModelLimit(nextLimit);
|
||||||
|
}
|
||||||
|
}, [collapsedLimit, expanded, maxCollapsedRows, measureTick, shouldCollapse]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!shouldCollapse || expanded || !maxCollapsedRows || typeof ResizeObserver === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = listRef.current;
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastWidth = container.clientWidth;
|
||||||
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
const width = Math.round(entries[0]?.contentRect.width ?? container.clientWidth);
|
||||||
|
if (width === lastWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastWidth = width;
|
||||||
|
setCollapsedModelLimit(null);
|
||||||
|
setMeasureTick((value) => value + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(container);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [expanded, maxCollapsedRows, shouldCollapse]);
|
||||||
|
|
||||||
const badgeClassName =
|
const badgeClassName =
|
||||||
'inline-flex items-center gap-1 rounded-md border px-1.5 py-px font-mono text-[10px] leading-4';
|
'inline-flex items-center gap-1 rounded-md border px-1.5 py-px font-mono text-[10px] leading-4';
|
||||||
|
|
@ -92,20 +179,18 @@ export const ProviderModelBadges = ({
|
||||||
};
|
};
|
||||||
const buttonClassName =
|
const buttonClassName =
|
||||||
'inline-flex items-center gap-1 rounded-full border border-[rgba(59,130,246,0.35)] bg-[rgba(59,130,246,0.12)] px-2 py-px text-[10px] font-medium leading-4 text-[rgb(147,197,253)] transition-colors hover:border-[rgba(59,130,246,0.55)] hover:bg-[rgba(59,130,246,0.18)] hover:text-[rgb(191,219,254)]';
|
'inline-flex items-center gap-1 rounded-full border border-[rgba(59,130,246,0.35)] bg-[rgba(59,130,246,0.12)] px-2 py-px text-[10px] font-medium leading-4 text-[rgb(147,197,253)] transition-colors hover:border-[rgba(59,130,246,0.55)] hover:bg-[rgba(59,130,246,0.18)] hover:text-[rgb(191,219,254)]';
|
||||||
const listClassName = cn('flex flex-wrap gap-1.5', expanded && shouldCollapse ? 'pr-1' : null);
|
const listClassName = cn('flex flex-wrap gap-1.5');
|
||||||
const listStyle =
|
|
||||||
expanded && shouldCollapse
|
|
||||||
? ({ maxHeight: expandedMaxHeightPx, overflowY: 'auto' } as const)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const renderModelBadge = (model: string, index: number): React.JSX.Element => {
|
const renderModelBadge = (model: string, index: number): React.JSX.Element => {
|
||||||
const availabilityStatus = getAvailabilityStatus(model, displayModelAvailability);
|
const availabilityStatus = getAvailabilityStatus(model, displayModelAvailability);
|
||||||
const availabilityReason = getAvailabilityReason(model, displayModelAvailability);
|
const availabilityReason = getAvailabilityReason(model, displayModelAvailability);
|
||||||
const availabilityChip = getAvailabilityChip(availabilityStatus);
|
const availabilityChip = getAvailabilityChip(availabilityStatus);
|
||||||
|
const modelLabel = formatModelBadgeLabel(providerId, model);
|
||||||
const catalogBadgeLabel = getCatalogBadgeLabel(model, providerStatus);
|
const catalogBadgeLabel = getCatalogBadgeLabel(model, providerStatus);
|
||||||
|
const showCatalogBadge = shouldRenderCatalogBadge(modelLabel, catalogBadgeLabel);
|
||||||
const title = [
|
const title = [
|
||||||
availabilityReason ?? availabilityChip,
|
availabilityReason ?? availabilityChip,
|
||||||
catalogBadgeLabel === 'Free'
|
showCatalogBadge && catalogBadgeLabel === 'Free'
|
||||||
? 'Reported by OpenCode metadata. Availability and limits may change.'
|
? 'Reported by OpenCode metadata. Availability and limits may change.'
|
||||||
: null,
|
: null,
|
||||||
]
|
]
|
||||||
|
|
@ -119,8 +204,8 @@ export const ProviderModelBadges = ({
|
||||||
style={badgeStyle}
|
style={badgeStyle}
|
||||||
title={title || undefined}
|
title={title || undefined}
|
||||||
>
|
>
|
||||||
<span>{formatModelBadgeLabel(providerId, model)}</span>
|
<span>{modelLabel}</span>
|
||||||
{catalogBadgeLabel ? (
|
{showCatalogBadge ? (
|
||||||
<span className="rounded bg-[rgba(34,197,94,0.14)] px-1 py-0 text-[9px] font-medium uppercase tracking-[0.06em] text-[rgb(74,222,128)]">
|
<span className="rounded bg-[rgba(34,197,94,0.14)] px-1 py-0 text-[9px] font-medium uppercase tracking-[0.06em] text-[rgb(74,222,128)]">
|
||||||
{catalogBadgeLabel}
|
{catalogBadgeLabel}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -149,7 +234,7 @@ export const ProviderModelBadges = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start gap-1.5">
|
<div className="flex flex-col items-start gap-1.5">
|
||||||
<div className={listClassName} style={listStyle}>
|
<div ref={listRef} className={listClassName}>
|
||||||
{displayedModels.map(renderModelBadge)}
|
{displayedModels.map(renderModelBadge)}
|
||||||
{shouldCollapse && !expanded ? (
|
{shouldCollapse && !expanded ? (
|
||||||
<button type="button" className={buttonClassName} onClick={() => setExpanded(true)}>
|
<button type="button" className={buttonClassName} onClick={() => setExpanded(true)}>
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ import type {
|
||||||
TeamCreateRequest,
|
TeamCreateRequest,
|
||||||
TeamLaunchRequest,
|
TeamLaunchRequest,
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
|
TeamSummary,
|
||||||
TeamTaskWithKanban,
|
TeamTaskWithKanban,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
import type { EditorSelectionAction } from '@shared/types/editor';
|
import type { EditorSelectionAction } from '@shared/types/editor';
|
||||||
|
|
@ -174,6 +175,61 @@ interface CreateTaskDialogState {
|
||||||
|
|
||||||
const TEAM_PENDING_REPLY_REFRESH_DELAY_MS = 10_000;
|
const TEAM_PENDING_REPLY_REFRESH_DELAY_MS = 10_000;
|
||||||
|
|
||||||
|
function getSummaryKnownTeammateCount(summary: TeamSummary | undefined): number {
|
||||||
|
if (!summary) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedLeadName = summary.leadName?.trim().toLowerCase();
|
||||||
|
const rosterNames = new Set<string>();
|
||||||
|
for (const member of summary.members ?? []) {
|
||||||
|
const name = member.name?.trim();
|
||||||
|
if (!name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalizedName = name.toLowerCase();
|
||||||
|
if (
|
||||||
|
normalizedName === 'user' ||
|
||||||
|
isLeadMember({ name }) ||
|
||||||
|
(normalizedLeadName && normalizedName === normalizedLeadName)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rosterNames.add(normalizedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchNames = new Set<string>();
|
||||||
|
for (const rawName of [...(summary.missingMembers ?? []), ...(summary.skippedMembers ?? [])]) {
|
||||||
|
const name = rawName.trim();
|
||||||
|
if (!name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalizedName = name.toLowerCase();
|
||||||
|
if (
|
||||||
|
normalizedName === 'user' ||
|
||||||
|
isLeadMember({ name }) ||
|
||||||
|
(normalizedLeadName && normalizedName === normalizedLeadName)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
launchNames.add(normalizedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeRosterCount = Math.max(summary.memberCount, rosterNames.size);
|
||||||
|
if (activeRosterCount > 0) {
|
||||||
|
return activeRosterCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(
|
||||||
|
summary.expectedMemberCount ?? 0,
|
||||||
|
launchNames.size,
|
||||||
|
(summary.confirmedCount ?? 0) +
|
||||||
|
(summary.pendingCount ?? 0) +
|
||||||
|
(summary.failedCount ?? 0) +
|
||||||
|
(summary.skippedCount ?? 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function areResolvedMembersEqual(
|
function areResolvedMembersEqual(
|
||||||
prev: readonly ResolvedTeamMember[],
|
prev: readonly ResolvedTeamMember[],
|
||||||
next: readonly ResolvedTeamMember[]
|
next: readonly ResolvedTeamMember[]
|
||||||
|
|
@ -1578,6 +1634,7 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
selectReviewFile,
|
selectReviewFile,
|
||||||
pendingReviewRequest,
|
pendingReviewRequest,
|
||||||
setPendingReviewRequest,
|
setPendingReviewRequest,
|
||||||
|
summaryKnownTeammateCount,
|
||||||
} = useStore(
|
} = useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
projects: s.projects,
|
projects: s.projects,
|
||||||
|
|
@ -1612,6 +1669,9 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
isTeamProvisioning: teamName ? isTeamProvisioningActive(s, teamName) : false,
|
isTeamProvisioning: teamName ? isTeamProvisioningActive(s, teamName) : false,
|
||||||
data: s.selectedTeamName === teamName ? s.selectedTeamData : null,
|
data: s.selectedTeamName === teamName ? s.selectedTeamData : null,
|
||||||
members: selectResolvedMembersForTeamName(s, teamName),
|
members: selectResolvedMembersForTeamName(s, teamName),
|
||||||
|
summaryKnownTeammateCount: teamName
|
||||||
|
? getSummaryKnownTeammateCount(s.teamByName[teamName])
|
||||||
|
: 0,
|
||||||
loading: s.selectedTeamName === teamName ? s.selectedTeamLoading : false,
|
loading: s.selectedTeamName === teamName ? s.selectedTeamLoading : false,
|
||||||
error: s.selectedTeamName === teamName ? s.selectedTeamError : null,
|
error: s.selectedTeamName === teamName ? s.selectedTeamError : null,
|
||||||
refreshTeamData: s.refreshTeamData,
|
refreshTeamData: s.refreshTeamData,
|
||||||
|
|
@ -1995,10 +2055,13 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
return filterKanbanTasks(filteredTasks, kanbanSearchQuery);
|
return filterKanbanTasks(filteredTasks, kanbanSearchQuery);
|
||||||
}, [filteredTasks, kanbanSearchQuery]);
|
}, [filteredTasks, kanbanSearchQuery]);
|
||||||
|
|
||||||
const activeTeammateCount = useMemo(
|
const activeTeammateCount = useMemo(() => {
|
||||||
() => activeMembers.filter((m) => !isLeadMember(m)).length,
|
const resolvedCount = activeMembers.filter((m) => !isLeadMember(m)).length;
|
||||||
[activeMembers]
|
if (membersWithLiveBranches.some((m) => m.removedAt)) {
|
||||||
);
|
return resolvedCount;
|
||||||
|
}
|
||||||
|
return resolvedCount > 0 ? resolvedCount : summaryKnownTeammateCount;
|
||||||
|
}, [activeMembers, membersWithLiveBranches, summaryKnownTeammateCount]);
|
||||||
const leadProviderId = useMemo<TeamProviderId | undefined>(() => {
|
const leadProviderId = useMemo<TeamProviderId | undefined>(() => {
|
||||||
const activeLeadProviderId = activeMembers.find(isLeadMember)?.providerId;
|
const activeLeadProviderId = activeMembers.find(isLeadMember)?.providerId;
|
||||||
if (activeLeadProviderId) return activeLeadProviderId;
|
if (activeLeadProviderId) return activeLeadProviderId;
|
||||||
|
|
@ -2814,6 +2877,7 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
<TeamMemberListBridge
|
<TeamMemberListBridge
|
||||||
teamName={teamName}
|
teamName={teamName}
|
||||||
members={membersWithLiveBranches}
|
members={membersWithLiveBranches}
|
||||||
|
expectedTeammateCount={activeTeammateCount}
|
||||||
memberTaskCounts={memberTaskCounts}
|
memberTaskCounts={memberTaskCounts}
|
||||||
taskMap={taskMap}
|
taskMap={taskMap}
|
||||||
pendingRepliesByMember={pendingRepliesByMember}
|
pendingRepliesByMember={pendingRepliesByMember}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import type {
|
||||||
interface MemberListProps {
|
interface MemberListProps {
|
||||||
teamName?: string;
|
teamName?: string;
|
||||||
members: ResolvedTeamMember[];
|
members: ResolvedTeamMember[];
|
||||||
|
expectedTeammateCount?: number;
|
||||||
memberTaskCounts?: Map<string, TaskStatusCounts>;
|
memberTaskCounts?: Map<string, TaskStatusCounts>;
|
||||||
taskMap?: Map<string, TeamTaskWithKanban>;
|
taskMap?: Map<string, TeamTaskWithKanban>;
|
||||||
pendingRepliesByMember?: Record<string, number>;
|
pendingRepliesByMember?: Record<string, number>;
|
||||||
|
|
@ -312,6 +313,7 @@ function areMemberListPropsEqual(
|
||||||
return (
|
return (
|
||||||
prev.teamName === next.teamName &&
|
prev.teamName === next.teamName &&
|
||||||
areResolvedMembersEquivalent(prev.members, next.members) &&
|
areResolvedMembersEquivalent(prev.members, next.members) &&
|
||||||
|
prev.expectedTeammateCount === next.expectedTeammateCount &&
|
||||||
areTaskStatusCountsMapsEquivalent(prev.memberTaskCounts, next.memberTaskCounts) &&
|
areTaskStatusCountsMapsEquivalent(prev.memberTaskCounts, next.memberTaskCounts) &&
|
||||||
areMemberTaskMapsEquivalent(prev.taskMap, next.taskMap) &&
|
areMemberTaskMapsEquivalent(prev.taskMap, next.taskMap) &&
|
||||||
arePendingRepliesEquivalent(prev.pendingRepliesByMember, next.pendingRepliesByMember) &&
|
arePendingRepliesEquivalent(prev.pendingRepliesByMember, next.pendingRepliesByMember) &&
|
||||||
|
|
@ -452,6 +454,7 @@ const MemberCardRow = memo(function MemberCardRow({
|
||||||
export const MemberList = memo(function MemberList({
|
export const MemberList = memo(function MemberList({
|
||||||
teamName = '__unknown_team__',
|
teamName = '__unknown_team__',
|
||||||
members,
|
members,
|
||||||
|
expectedTeammateCount,
|
||||||
memberTaskCounts,
|
memberTaskCounts,
|
||||||
taskMap,
|
taskMap,
|
||||||
pendingRepliesByMember,
|
pendingRepliesByMember,
|
||||||
|
|
@ -502,6 +505,10 @@ export const MemberList = memo(function MemberList({
|
||||||
[members]
|
[members]
|
||||||
);
|
);
|
||||||
const removedMembers = useMemo(() => members.filter((m) => m.removedAt), [members]);
|
const removedMembers = useMemo(() => members.filter((m) => m.removedAt), [members]);
|
||||||
|
const activeTeammateCount = useMemo(
|
||||||
|
() => activeMembers.filter((member) => !isLeadMember(member)).length,
|
||||||
|
[activeMembers]
|
||||||
|
);
|
||||||
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
|
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
|
||||||
// Pre-compute reviewer->task map to avoid O(n*n) scan per member.
|
// Pre-compute reviewer->task map to avoid O(n*n) scan per member.
|
||||||
const reviewTaskByMember = useMemo(() => {
|
const reviewTaskByMember = useMemo(() => {
|
||||||
|
|
@ -644,10 +651,22 @@ export const MemberList = memo(function MemberList({
|
||||||
[launchParams]
|
[launchParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (members.length === 0) {
|
const expectsTeammates = (expectedTeammateCount ?? 0) > 0;
|
||||||
|
const hasOnlyLeadWhileTeammatesLoad =
|
||||||
|
expectsTeammates && activeTeammateCount === 0 && removedMembers.length === 0;
|
||||||
|
|
||||||
|
if (members.length === 0 || hasOnlyLeadWhileTeammatesLoad) {
|
||||||
|
if (expectsTeammates) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-[var(--color-border)] p-4 text-sm text-[var(--color-text-muted)]">
|
<div className="rounded-md border border-[var(--color-border)] p-4 text-sm text-[var(--color-text-muted)]">
|
||||||
Solo team — lead only
|
Team members are loading
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-[var(--color-border)] p-4 text-sm text-[var(--color-text-muted)]">
|
||||||
|
Solo team - lead only
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,9 @@ export function buildTeamChangeRequestPlan(
|
||||||
}
|
}
|
||||||
seenTaskIds.add(task.id);
|
seenTaskIds.add(task.id);
|
||||||
|
|
||||||
const options = buildTaskChangeRequestOptions(task, { summaryOnly: true });
|
const requestOptions = buildTaskChangeRequestOptions(task, { summaryOnly: true });
|
||||||
const presence = task.changePresence ?? 'unknown';
|
const presence = task.changePresence ?? 'unknown';
|
||||||
const canDisplay = canDisplayTaskChangesForOptions(options);
|
const canDisplay = canDisplayTaskChangesForOptions(requestOptions);
|
||||||
const shouldScanUnknown =
|
const shouldScanUnknown =
|
||||||
presence === 'unknown' && (canDisplay || hasTaskChangeScanEvidence(task));
|
presence === 'unknown' && (canDisplay || hasTaskChangeScanEvidence(task));
|
||||||
if (
|
if (
|
||||||
|
|
@ -125,19 +125,19 @@ export function buildTeamChangeRequestPlan(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (presence === 'has_changes') {
|
if (presence === 'has_changes') {
|
||||||
primary.push({ task, options, priority: 0 });
|
primary.push({ task, options: requestOptions, priority: 0 });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (presence === 'needs_attention') {
|
if (presence === 'needs_attention') {
|
||||||
primary.push({ task, options, priority: 1 });
|
primary.push({ task, options: requestOptions, priority: 1 });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (options.stateBucket === 'active' && options.status === 'in_progress') {
|
if (requestOptions.stateBucket === 'active' && requestOptions.status === 'in_progress') {
|
||||||
active.push({ task, options, priority: 2 });
|
active.push({ task, options: requestOptions, priority: 2 });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (shouldScanUnknown) {
|
if (shouldScanUnknown) {
|
||||||
unknown.push({ task, options, priority: 3 });
|
unknown.push({ task, options: requestOptions, priority: 3 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,15 +166,15 @@ export function buildTeamChangeRequestPlan(
|
||||||
const selected = [...requestPrimary, ...requestActive, ...unknownWindow].slice(0, maxRequests);
|
const selected = [...requestPrimary, ...requestActive, ...unknownWindow].slice(0, maxRequests);
|
||||||
const requestOptionsByTaskId = new Map<string, TaskChangeRequestOptions>();
|
const requestOptionsByTaskId = new Map<string, TaskChangeRequestOptions>();
|
||||||
const requests = selected.map((candidate) => {
|
const requests = selected.map((candidate) => {
|
||||||
const options = {
|
const requestOptions = {
|
||||||
...candidate.options,
|
...candidate.options,
|
||||||
summaryOnly: true,
|
summaryOnly: true,
|
||||||
forceFresh: forceFresh ? true : candidate.options.forceFresh,
|
forceFresh: forceFresh ? true : candidate.options.forceFresh,
|
||||||
};
|
};
|
||||||
requestOptionsByTaskId.set(candidate.task.id, options);
|
requestOptionsByTaskId.set(candidate.task.id, requestOptions);
|
||||||
return {
|
return {
|
||||||
taskId: candidate.task.id,
|
taskId: candidate.task.id,
|
||||||
options,
|
options: requestOptions,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const eligibleCount = primary.length + active.length + unknown.length;
|
const eligibleCount = primary.length + active.length + unknown.length;
|
||||||
|
|
|
||||||
|
|
@ -196,8 +196,7 @@ function isDocumentHidden(): boolean {
|
||||||
|
|
||||||
function isSilentCounterLoad(options: TeamChangesLoadOptions | null): boolean {
|
function isSilentCounterLoad(options: TeamChangesLoadOptions | null): boolean {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
options &&
|
options?.storeSummaries === false &&
|
||||||
options.storeSummaries === false &&
|
|
||||||
options.reportError === false &&
|
options.reportError === false &&
|
||||||
options.showSpinner !== true
|
options.showSpinner !== true
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1983,6 +1983,165 @@ function buildConfigFallbackMemberSnapshots(snapshot: TeamViewSnapshot): TeamMem
|
||||||
return fallbackMembers;
|
return fallbackMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActiveRawTeammateNameKeys(snapshot: TeamViewSnapshot | null | undefined): string[] {
|
||||||
|
if (!snapshot) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const names = new Set<string>();
|
||||||
|
for (const member of snapshot.members) {
|
||||||
|
const name = member.name.trim();
|
||||||
|
const key = name.toLowerCase();
|
||||||
|
if (!name || key === 'user' || member.removedAt || isLeadMember(member)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
names.add(key);
|
||||||
|
}
|
||||||
|
return Array.from(names).sort((left, right) => left.localeCompare(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasActiveRawTeammateRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
||||||
|
return getActiveRawTeammateNameKeys(snapshot).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRemovedRawMemberRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
||||||
|
return Boolean(snapshot?.members.some((member) => member.removedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasConfigTeammateRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
||||||
|
return Boolean(
|
||||||
|
snapshot?.config.members?.some((member) => {
|
||||||
|
const name = member.name?.trim();
|
||||||
|
return Boolean(name) && !member.removedAt && !isLeadMember(member);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummaryFallbackMemberSource {
|
||||||
|
name: string;
|
||||||
|
agentId?: string;
|
||||||
|
role?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSummaryTeammateName(
|
||||||
|
name: string | undefined | null,
|
||||||
|
leadName?: string
|
||||||
|
): string | null {
|
||||||
|
const trimmed = name?.trim();
|
||||||
|
const normalizedName = trimmed?.toLowerCase();
|
||||||
|
const normalizedLeadName = leadName?.trim().toLowerCase();
|
||||||
|
if (
|
||||||
|
!trimmed ||
|
||||||
|
normalizedName === 'user' ||
|
||||||
|
isLeadMember({ name: trimmed }) ||
|
||||||
|
(normalizedLeadName && normalizedName === normalizedLeadName)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSummaryRosterTeammateSources(summary: TeamSummary): SummaryFallbackMemberSource[] {
|
||||||
|
const seenNames = new Set<string>();
|
||||||
|
const sources: SummaryFallbackMemberSource[] = [];
|
||||||
|
for (const member of summary.members ?? []) {
|
||||||
|
const name = normalizeSummaryTeammateName(member.name, summary.leadName);
|
||||||
|
if (!name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = name.toLowerCase();
|
||||||
|
if (seenNames.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenNames.add(key);
|
||||||
|
sources.push({
|
||||||
|
name,
|
||||||
|
agentId: member.agentId,
|
||||||
|
role: member.role,
|
||||||
|
color: member.color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseSummaryLaunchTeammateSources(summary: TeamSummary): boolean {
|
||||||
|
return (
|
||||||
|
summary.partialLaunchFailure === true ||
|
||||||
|
summary.teamLaunchState === 'partial_failure' ||
|
||||||
|
summary.teamLaunchState === 'partial_pending' ||
|
||||||
|
summary.teamLaunchState === 'partial_skipped'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSummaryLaunchTeammateSources(summary: TeamSummary): SummaryFallbackMemberSource[] {
|
||||||
|
if (!shouldUseSummaryLaunchTeammateSources(summary)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenNames = new Set<string>();
|
||||||
|
const sources: SummaryFallbackMemberSource[] = [];
|
||||||
|
for (const rawName of [...(summary.missingMembers ?? []), ...(summary.skippedMembers ?? [])]) {
|
||||||
|
const name = normalizeSummaryTeammateName(rawName, summary.leadName);
|
||||||
|
if (!name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = name.toLowerCase();
|
||||||
|
if (seenNames.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenNames.add(key);
|
||||||
|
sources.push({ name });
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSummaryLaunchTeammateNameKeys(summary: TeamSummary): string[] {
|
||||||
|
return getSummaryLaunchTeammateSources(summary)
|
||||||
|
.map((member) => member.name.toLowerCase())
|
||||||
|
.sort((left, right) => left.localeCompare(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSummaryTeammateNameKeys(summary: TeamSummary): string[] {
|
||||||
|
const rosterNames = getSummaryRosterTeammateSources(summary)
|
||||||
|
.map((member) => member.name.toLowerCase())
|
||||||
|
.sort((left, right) => left.localeCompare(right));
|
||||||
|
if (rosterNames.length > 0) {
|
||||||
|
return rosterNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchNames = getSummaryLaunchTeammateNameKeys(summary);
|
||||||
|
const expectedCount = summary.expectedMemberCount ?? summary.memberCount;
|
||||||
|
if (expectedCount > 0 && launchNames.length === expectedCount) {
|
||||||
|
return launchNames;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSummaryFallbackTeammateSources(summary: TeamSummary): SummaryFallbackMemberSource[] {
|
||||||
|
return getSummaryRosterTeammateSources(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function areNameKeyListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
||||||
|
return left.length === right.length && left.every((name, index) => name === right[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function summaryConfirmsActiveTeammateRoster(
|
||||||
|
current: TeamViewSnapshot,
|
||||||
|
summary: TeamSummary
|
||||||
|
): boolean {
|
||||||
|
if ((summary.expectedMemberCount ?? summary.memberCount) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentNames = getActiveRawTeammateNameKeys(current);
|
||||||
|
const summaryNames = getSummaryTeammateNameKeys(summary);
|
||||||
|
if (summaryNames.length === 0 || summaryNames.length !== currentNames.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return areNameKeyListsEqual(summaryNames, currentNames);
|
||||||
|
}
|
||||||
|
|
||||||
function buildSummaryFallbackMemberSnapshots(
|
function buildSummaryFallbackMemberSnapshots(
|
||||||
snapshot: TeamViewSnapshot,
|
snapshot: TeamViewSnapshot,
|
||||||
summary: TeamSummary | undefined
|
summary: TeamSummary | undefined
|
||||||
|
|
@ -1990,8 +2149,8 @@ function buildSummaryFallbackMemberSnapshots(
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const summaryMembers = summary.members ?? [];
|
const summaryMembers = getSummaryFallbackTeammateSources(summary);
|
||||||
if (summaryMembers.length === 0 || summary.memberCount <= 0) {
|
if (summaryMembers.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2021,11 +2180,7 @@ function buildSummaryFallbackMemberSnapshots(
|
||||||
};
|
};
|
||||||
|
|
||||||
const teammates = summaryMembers.flatMap((member) => {
|
const teammates = summaryMembers.flatMap((member) => {
|
||||||
const name = member.name?.trim();
|
const item = buildSnapshot(member.name, member);
|
||||||
if (!name || name === 'user' || isLeadMember(member)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const item = buildSnapshot(name, member);
|
|
||||||
return item ? [item] : [];
|
return item ? [item] : [];
|
||||||
});
|
});
|
||||||
if (teammates.length === 0) {
|
if (teammates.length === 0) {
|
||||||
|
|
@ -2080,71 +2235,6 @@ function getResolvableMemberSnapshots(
|
||||||
return snapshot.members;
|
return snapshot.members;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveRawTeammateNameKeys(snapshot: TeamViewSnapshot | null | undefined): string[] {
|
|
||||||
if (!snapshot) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const names = new Set<string>();
|
|
||||||
for (const member of snapshot.members) {
|
|
||||||
const name = member.name.trim();
|
|
||||||
if (!name || name === 'user' || member.removedAt || isLeadMember(member)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
names.add(name.toLowerCase());
|
|
||||||
}
|
|
||||||
return Array.from(names).sort((left, right) => left.localeCompare(right));
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasActiveRawTeammateRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
|
||||||
return getActiveRawTeammateNameKeys(snapshot).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasRemovedRawMemberRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
|
||||||
return Boolean(snapshot?.members.some((member) => member.removedAt));
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasConfigTeammateRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
|
||||||
return Boolean(
|
|
||||||
snapshot?.config.members?.some((member) => {
|
|
||||||
const name = member.name?.trim();
|
|
||||||
return Boolean(name) && !member.removedAt && !isLeadMember(member);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSummaryTeammateNameKeys(summary: TeamSummary): string[] {
|
|
||||||
const names = new Set<string>();
|
|
||||||
for (const member of summary.members ?? []) {
|
|
||||||
const name = member.name?.trim();
|
|
||||||
if (!name || name === 'user' || isLeadMember(member)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
names.add(name.toLowerCase());
|
|
||||||
}
|
|
||||||
return Array.from(names).sort((left, right) => left.localeCompare(right));
|
|
||||||
}
|
|
||||||
|
|
||||||
function areNameKeyListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
|
||||||
return left.length === right.length && left.every((name, index) => name === right[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function summaryConfirmsActiveTeammateRoster(
|
|
||||||
current: TeamViewSnapshot,
|
|
||||||
summary: TeamSummary
|
|
||||||
): boolean {
|
|
||||||
if (summary.memberCount <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentNames = getActiveRawTeammateNameKeys(current);
|
|
||||||
const summaryNames = getSummaryTeammateNameKeys(summary);
|
|
||||||
if (summaryNames.length === 0 || summaryNames.length !== currentNames.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return areNameKeyListsEqual(summaryNames, currentNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldPreserveSelectedTeamSnapshot(
|
function shouldPreserveSelectedTeamSnapshot(
|
||||||
current: TeamViewSnapshot | null,
|
current: TeamViewSnapshot | null,
|
||||||
baseline: TeamViewSnapshot | null | undefined,
|
baseline: TeamViewSnapshot | null | undefined,
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,7 @@ function getRuntimeSelectorModels(
|
||||||
return getVisibleTeamProviderModels(providerId, catalogModels, providerStatus);
|
return getVisibleTeamProviderModels(providerId, catalogModels, providerStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortTeamProviderModels(providerId, providerStatus.models);
|
return sortTeamProviderModels(providerId, providerStatus.models, providerStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVisibleRuntimeModels(
|
function getVisibleRuntimeModels(
|
||||||
|
|
|
||||||
|
|
@ -400,9 +400,38 @@ export function getRuntimeAwareTeamModelBadgeLabel(
|
||||||
return getTeamModelBadgeLabel(providerId, model);
|
return getTeamModelBadgeLabel(providerId, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasExplicitFreeOpenCodeModelMarker(model: string): boolean {
|
||||||
|
const normalized = model.trim().toLowerCase();
|
||||||
|
return (
|
||||||
|
normalized === 'opencode/big-pickle' ||
|
||||||
|
normalized.includes(':free') ||
|
||||||
|
normalized.endsWith('-free') ||
|
||||||
|
normalized.endsWith('/free')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFreeOpenCodeModelForOrdering(
|
||||||
|
providerId: SupportedProviderId,
|
||||||
|
model: string,
|
||||||
|
providerStatus?: RuntimeAwareProviderStatus | null
|
||||||
|
): boolean {
|
||||||
|
if (providerId !== 'opencode') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
||||||
|
const badgeLabel = runtimeModel?.badgeLabel?.trim().toLowerCase();
|
||||||
|
if (badgeLabel) {
|
||||||
|
return badgeLabel === 'free';
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasExplicitFreeOpenCodeModelMarker(model);
|
||||||
|
}
|
||||||
|
|
||||||
export function sortTeamProviderModels(
|
export function sortTeamProviderModels(
|
||||||
providerId: SupportedProviderId,
|
providerId: SupportedProviderId,
|
||||||
models: readonly string[]
|
models: readonly string[],
|
||||||
|
providerStatus?: RuntimeAwareProviderStatus | null
|
||||||
): string[] {
|
): string[] {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const deduped = models.filter((model) => {
|
const deduped = models.filter((model) => {
|
||||||
|
|
@ -415,7 +444,7 @@ export function sortTeamProviderModels(
|
||||||
});
|
});
|
||||||
const order = TEAM_PROVIDER_MODEL_ORDER[providerId];
|
const order = TEAM_PROVIDER_MODEL_ORDER[providerId];
|
||||||
|
|
||||||
return [...deduped].sort((left, right) => {
|
const sorted = [...deduped].sort((left, right) => {
|
||||||
const leftRank = order.get(left) ?? Number.MAX_SAFE_INTEGER;
|
const leftRank = order.get(left) ?? Number.MAX_SAFE_INTEGER;
|
||||||
const rightRank = order.get(right) ?? Number.MAX_SAFE_INTEGER;
|
const rightRank = order.get(right) ?? Number.MAX_SAFE_INTEGER;
|
||||||
if (leftRank !== rightRank) {
|
if (leftRank !== rightRank) {
|
||||||
|
|
@ -423,6 +452,22 @@ export function sortTeamProviderModels(
|
||||||
}
|
}
|
||||||
return left.localeCompare(right);
|
return left.localeCompare(right);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (providerId !== 'opencode') {
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted
|
||||||
|
.map((model, index) => ({ model, index }))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftFree = isFreeOpenCodeModelForOrdering(providerId, left.model, providerStatus);
|
||||||
|
const rightFree = isFreeOpenCodeModelForOrdering(providerId, right.model, providerStatus);
|
||||||
|
if (leftFree !== rightFree) {
|
||||||
|
return leftFree ? -1 : 1;
|
||||||
|
}
|
||||||
|
return left.index - right.index;
|
||||||
|
})
|
||||||
|
.map((entry) => entry.model);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCodexChatGptSubscriptionProviderStatus(
|
export function isCodexChatGptSubscriptionProviderStatus(
|
||||||
|
|
@ -468,7 +513,11 @@ export function getVisibleTeamProviderModels(
|
||||||
): string[] {
|
): string[] {
|
||||||
return sortTeamProviderModels(
|
return sortTeamProviderModels(
|
||||||
providerId,
|
providerId,
|
||||||
filterVisibleProviderRuntimeModels(providerId, getSupplementalVisibleModels(providerId, models))
|
filterVisibleProviderRuntimeModels(
|
||||||
|
providerId,
|
||||||
|
getSupplementalVisibleModels(providerId, models)
|
||||||
|
),
|
||||||
|
providerStatus
|
||||||
).filter((model) => !isRuntimeHiddenTeamModel(providerId, model, providerStatus));
|
).filter((model) => !isRuntimeHiddenTeamModel(providerId, model, providerStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,12 @@ export interface CliProviderModelCatalogItem {
|
||||||
source: CliProviderModelCatalogSource;
|
source: CliProviderModelCatalogSource;
|
||||||
badgeLabel?: string | null;
|
badgeLabel?: string | null;
|
||||||
statusMessage?: string | null;
|
statusMessage?: string | null;
|
||||||
|
metadata?: {
|
||||||
|
cost?: unknown;
|
||||||
|
context?: number | null;
|
||||||
|
limits?: unknown;
|
||||||
|
free?: boolean;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CliProviderModelCatalog {
|
export interface CliProviderModelCatalog {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
} from '@features/codex-account/contracts';
|
} from '@features/codex-account/contracts';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
apiKeyHasPreferredMock,
|
||||||
apiKeyLookupMock,
|
apiKeyLookupMock,
|
||||||
binaryResolveMock,
|
binaryResolveMock,
|
||||||
detectLocalAccountStateMock,
|
detectLocalAccountStateMock,
|
||||||
|
|
@ -25,6 +26,7 @@ const {
|
||||||
readRateLimitsMock,
|
readRateLimitsMock,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
binaryResolveMock: vi.fn(),
|
binaryResolveMock: vi.fn(),
|
||||||
|
apiKeyHasPreferredMock: vi.fn(),
|
||||||
apiKeyLookupMock: vi.fn(),
|
apiKeyLookupMock: vi.fn(),
|
||||||
detectLocalAccountStateMock: vi.fn(),
|
detectLocalAccountStateMock: vi.fn(),
|
||||||
getCachedShellEnvMock: vi.fn(),
|
getCachedShellEnvMock: vi.fn(),
|
||||||
|
|
@ -59,6 +61,7 @@ function emitLoginState(nextState: CodexLoginStateDto): void {
|
||||||
|
|
||||||
vi.mock('../../../../src/main/services/extensions', () => ({
|
vi.mock('../../../../src/main/services/extensions', () => ({
|
||||||
ApiKeyService: class MockApiKeyService {
|
ApiKeyService: class MockApiKeyService {
|
||||||
|
hasPreferred = apiKeyHasPreferredMock;
|
||||||
lookupPreferred = apiKeyLookupMock;
|
lookupPreferred = apiKeyLookupMock;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
@ -228,6 +231,7 @@ describe('createCodexAccountFeature', () => {
|
||||||
delete process.env.OPENAI_API_KEY;
|
delete process.env.OPENAI_API_KEY;
|
||||||
delete process.env.CODEX_API_KEY;
|
delete process.env.CODEX_API_KEY;
|
||||||
binaryResolveMock.mockResolvedValue('/usr/local/bin/codex');
|
binaryResolveMock.mockResolvedValue('/usr/local/bin/codex');
|
||||||
|
apiKeyHasPreferredMock.mockResolvedValue(false);
|
||||||
apiKeyLookupMock.mockResolvedValue(null);
|
apiKeyLookupMock.mockResolvedValue(null);
|
||||||
detectLocalAccountStateMock.mockResolvedValue({
|
detectLocalAccountStateMock.mockResolvedValue({
|
||||||
hasArtifacts: false,
|
hasArtifacts: false,
|
||||||
|
|
|
||||||
|
|
@ -36,21 +36,22 @@ describe('ApiKeyService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('persists projectPath for project-scoped API keys', async () => {
|
it('persists projectPath for project-scoped API keys', async () => {
|
||||||
|
const projectPath = path.join(tempDir, 'project-a');
|
||||||
const saved = await service.save({
|
const saved = await service.save({
|
||||||
name: 'Project Tavily',
|
name: 'Project Tavily',
|
||||||
envVarName: 'TAVILY_API_KEY',
|
envVarName: 'TAVILY_API_KEY',
|
||||||
value: 'secret',
|
value: 'secret',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
projectPath: '/tmp/project-a',
|
projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(saved.scope).toBe('project');
|
expect(saved.scope).toBe('project');
|
||||||
expect(saved.projectPath).toBe('/tmp/project-a');
|
expect(saved.projectPath).toBe(projectPath);
|
||||||
|
|
||||||
await expect(service.list()).resolves.toEqual([
|
await expect(service.list()).resolves.toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
projectPath: '/tmp/project-a',
|
projectPath,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
@ -67,6 +68,7 @@ describe('ApiKeyService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prefers exact project matches over user keys during lookup', async () => {
|
it('prefers exact project matches over user keys during lookup', async () => {
|
||||||
|
const projectPath = path.join(tempDir, 'project-a');
|
||||||
await service.save({
|
await service.save({
|
||||||
name: 'Shared Tavily',
|
name: 'Shared Tavily',
|
||||||
envVarName: 'TAVILY_API_KEY',
|
envVarName: 'TAVILY_API_KEY',
|
||||||
|
|
@ -78,10 +80,10 @@ describe('ApiKeyService', () => {
|
||||||
envVarName: 'TAVILY_API_KEY',
|
envVarName: 'TAVILY_API_KEY',
|
||||||
value: 'project-secret',
|
value: 'project-secret',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
projectPath: '/tmp/project-a',
|
projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(service.lookup(['TAVILY_API_KEY'], '/tmp/project-a')).resolves.toEqual([
|
await expect(service.lookup(['TAVILY_API_KEY'], projectPath)).resolves.toEqual([
|
||||||
{
|
{
|
||||||
envVarName: 'TAVILY_API_KEY',
|
envVarName: 'TAVILY_API_KEY',
|
||||||
value: 'project-secret',
|
value: 'project-secret',
|
||||||
|
|
@ -90,6 +92,8 @@ describe('ApiKeyService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to user keys when project-specific matches do not exist', async () => {
|
it('falls back to user keys when project-specific matches do not exist', async () => {
|
||||||
|
const projectPath = path.join(tempDir, 'project-a');
|
||||||
|
const otherProjectPath = path.join(tempDir, 'project-b');
|
||||||
await service.save({
|
await service.save({
|
||||||
name: 'Shared Tavily',
|
name: 'Shared Tavily',
|
||||||
envVarName: 'TAVILY_API_KEY',
|
envVarName: 'TAVILY_API_KEY',
|
||||||
|
|
@ -101,10 +105,10 @@ describe('ApiKeyService', () => {
|
||||||
envVarName: 'TAVILY_API_KEY',
|
envVarName: 'TAVILY_API_KEY',
|
||||||
value: 'project-secret',
|
value: 'project-secret',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
projectPath: '/tmp/project-b',
|
projectPath: otherProjectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(service.lookup(['TAVILY_API_KEY'], '/tmp/project-a')).resolves.toEqual([
|
await expect(service.lookup(['TAVILY_API_KEY'], projectPath)).resolves.toEqual([
|
||||||
{
|
{
|
||||||
envVarName: 'TAVILY_API_KEY',
|
envVarName: 'TAVILY_API_KEY',
|
||||||
value: 'user-secret',
|
value: 'user-secret',
|
||||||
|
|
@ -113,18 +117,38 @@ describe('ApiKeyService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not leak project-scoped keys without project context', async () => {
|
it('does not leak project-scoped keys without project context', async () => {
|
||||||
|
const projectPath = path.join(tempDir, 'project-a');
|
||||||
await service.save({
|
await service.save({
|
||||||
name: 'Project only key',
|
name: 'Project only key',
|
||||||
envVarName: 'TAVILY_API_KEY',
|
envVarName: 'TAVILY_API_KEY',
|
||||||
value: 'project-secret',
|
value: 'project-secret',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
projectPath: '/tmp/project-a',
|
projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(service.lookup(['TAVILY_API_KEY'])).resolves.toEqual([]);
|
await expect(service.lookup(['TAVILY_API_KEY'])).resolves.toEqual([]);
|
||||||
await expect(service.lookupPreferred('TAVILY_API_KEY')).resolves.toBeNull();
|
await expect(service.lookupPreferred('TAVILY_API_KEY')).resolves.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('checks preferred key presence without decrypting the stored value', async () => {
|
||||||
|
vi.mocked(safeStorage.isEncryptionAvailable).mockReturnValue(true);
|
||||||
|
vi.mocked(safeStorage.getSelectedStorageBackend).mockReturnValue('gnome_libsecret');
|
||||||
|
vi.mocked(safeStorage.encryptString).mockReturnValue(Buffer.from('encrypted-value'));
|
||||||
|
|
||||||
|
await service.save({
|
||||||
|
name: 'Anthropic API Key',
|
||||||
|
envVarName: 'ANTHROPIC_API_KEY',
|
||||||
|
value: 'secret',
|
||||||
|
scope: 'user',
|
||||||
|
});
|
||||||
|
vi.mocked(safeStorage.decryptString).mockImplementation(() => {
|
||||||
|
throw new Error('decrypt should not be called');
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(service.hasPreferred('ANTHROPIC_API_KEY')).resolves.toBe(true);
|
||||||
|
expect(safeStorage.decryptString).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('does not print decrypt failures to the normal console', async () => {
|
it('does not print decrypt failures to the normal console', async () => {
|
||||||
vi.mocked(safeStorage.isEncryptionAvailable).mockReturnValue(true);
|
vi.mocked(safeStorage.isEncryptionAvailable).mockReturnValue(true);
|
||||||
vi.mocked(safeStorage.getSelectedStorageBackend).mockReturnValue('gnome_libsecret');
|
vi.mocked(safeStorage.getSelectedStorageBackend).mockReturnValue('gnome_libsecret');
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,70 @@
|
||||||
|
import { createHash } from 'crypto';
|
||||||
import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
|
import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
import { gzipSync } from 'zlib';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { resolveAppManagedOpenCodeRuntimeBinaryPath } from '@main/services/infrastructure/OpenCodeRuntimeInstallerService';
|
const execCliMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock('@main/utils/childProcess', () => ({
|
||||||
|
execCli: execCliMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
extractOpenCodeRuntimeBinaryFromTarball,
|
||||||
|
getOpenCodeRuntimePlatformCandidates,
|
||||||
|
resolveAppManagedOpenCodeRuntimeBinaryPath,
|
||||||
|
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath,
|
||||||
|
verifyOpenCodeRuntimePackageIntegrity,
|
||||||
|
} from '@main/services/infrastructure/OpenCodeRuntimeInstallerService';
|
||||||
import { setAppDataBasePath } from '@main/utils/pathDecoder';
|
import { setAppDataBasePath } from '@main/utils/pathDecoder';
|
||||||
|
|
||||||
let tempRoot: string | null = null;
|
let tempRoot: string | null = null;
|
||||||
|
|
||||||
|
function writeOctal(header: Buffer, offset: number, length: number, value: number): void {
|
||||||
|
const encoded = value
|
||||||
|
.toString(8)
|
||||||
|
.padStart(length - 1, '0')
|
||||||
|
.slice(-(length - 1));
|
||||||
|
header.write(`${encoded}\0`, offset, length, 'ascii');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTarEntry(name: string, data: Buffer): Buffer {
|
||||||
|
const header = Buffer.alloc(512);
|
||||||
|
header.write(name, 0, Math.min(Buffer.byteLength(name), 100), 'utf8');
|
||||||
|
writeOctal(header, 100, 8, 0o755);
|
||||||
|
writeOctal(header, 108, 8, 0);
|
||||||
|
writeOctal(header, 116, 8, 0);
|
||||||
|
writeOctal(header, 124, 12, data.length);
|
||||||
|
writeOctal(header, 136, 12, 0);
|
||||||
|
header.fill(' ', 148, 156);
|
||||||
|
header.write('0', 156, 1, 'ascii');
|
||||||
|
header.write('ustar\0', 257, 6, 'ascii');
|
||||||
|
header.write('00', 263, 2, 'ascii');
|
||||||
|
const checksum = header.reduce((sum, byte) => sum + byte, 0);
|
||||||
|
const checksumText = checksum.toString(8).padStart(6, '0');
|
||||||
|
header.write(`${checksumText}\0 `, 148, 8, 'ascii');
|
||||||
|
|
||||||
|
const padding = Buffer.alloc((512 - (data.length % 512)) % 512);
|
||||||
|
return Buffer.concat([header, data, padding]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTarball(entries: { name: string; data: string }[]): Buffer {
|
||||||
|
return gzipSync(
|
||||||
|
Buffer.concat([
|
||||||
|
...entries.map((entry) => createTarEntry(entry.name, Buffer.from(entry.data))),
|
||||||
|
Buffer.alloc(1024),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe('OpenCodeRuntimeInstallerService resolver', () => {
|
describe('OpenCodeRuntimeInstallerService resolver', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-runtime-resolver-'));
|
tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-runtime-resolver-'));
|
||||||
setAppDataBasePath(tempRoot);
|
setAppDataBasePath(tempRoot);
|
||||||
|
execCliMock.mockReset();
|
||||||
|
execCliMock.mockResolvedValue({ stdout: 'opencode 1.0.0\n', stderr: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
@ -71,4 +124,111 @@ describe('OpenCodeRuntimeInstallerService resolver', () => {
|
||||||
|
|
||||||
expect(resolveAppManagedOpenCodeRuntimeBinaryPath()).toBeNull();
|
expect(resolveAppManagedOpenCodeRuntimeBinaryPath()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns the verified app-managed binary path only when --version succeeds', async () => {
|
||||||
|
const binaryPath = path.join(
|
||||||
|
tempRoot!,
|
||||||
|
'data',
|
||||||
|
'runtimes',
|
||||||
|
'opencode',
|
||||||
|
'versions',
|
||||||
|
'1.0.0',
|
||||||
|
'opencode-test',
|
||||||
|
'opencode'
|
||||||
|
);
|
||||||
|
const manifestPath = path.join(tempRoot!, 'data', 'runtimes', 'opencode', 'current.json');
|
||||||
|
await mkdir(path.dirname(binaryPath), { recursive: true });
|
||||||
|
await mkdir(path.dirname(manifestPath), { recursive: true });
|
||||||
|
await writeFile(binaryPath, 'binary', { mode: 0o755 });
|
||||||
|
await writeFile(
|
||||||
|
manifestPath,
|
||||||
|
`${JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
version: '1.0.0',
|
||||||
|
platformPackage: 'opencode-test',
|
||||||
|
binaryPath,
|
||||||
|
integrity: 'sha512-test',
|
||||||
|
installedAt: '2026-05-12T00:00:00.000Z',
|
||||||
|
})}\n`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()).resolves.toBe(binaryPath);
|
||||||
|
expect(execCliMock).toHaveBeenCalledWith(binaryPath, ['--version'], {
|
||||||
|
timeout: 10_000,
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
execCliMock.mockRejectedValueOnce(new Error('broken binary'));
|
||||||
|
|
||||||
|
await expect(resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OpenCodeRuntimeInstallerService package safety helpers', () => {
|
||||||
|
it('selects expected platform packages with Linux musl and baseline fallbacks', () => {
|
||||||
|
expect(
|
||||||
|
getOpenCodeRuntimePlatformCandidates('darwin', 'arm64', false).map((item) => item.packageName)
|
||||||
|
).toEqual(['opencode-darwin-arm64']);
|
||||||
|
expect(
|
||||||
|
getOpenCodeRuntimePlatformCandidates('darwin', 'x64', false).map((item) => item.packageName)
|
||||||
|
).toEqual(['opencode-darwin-x64', 'opencode-darwin-x64-baseline']);
|
||||||
|
expect(
|
||||||
|
getOpenCodeRuntimePlatformCandidates('linux', 'x64', false).map((item) => item.packageName)
|
||||||
|
).toEqual(['opencode-linux-x64', 'opencode-linux-x64-baseline', 'opencode-linux-x64-musl']);
|
||||||
|
expect(
|
||||||
|
getOpenCodeRuntimePlatformCandidates('linux', 'x64', true).map((item) => item.packageName)
|
||||||
|
).toEqual([
|
||||||
|
'opencode-linux-x64-musl',
|
||||||
|
'opencode-linux-x64-baseline-musl',
|
||||||
|
'opencode-linux-x64',
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
getOpenCodeRuntimePlatformCandidates('linux', 'arm64', false).map((item) => item.packageName)
|
||||||
|
).toEqual(['opencode-linux-arm64', 'opencode-linux-arm64-musl']);
|
||||||
|
expect(
|
||||||
|
getOpenCodeRuntimePlatformCandidates('linux', 'arm64', true).map((item) => item.packageName)
|
||||||
|
).toEqual(['opencode-linux-arm64-musl', 'opencode-linux-arm64']);
|
||||||
|
expect(
|
||||||
|
getOpenCodeRuntimePlatformCandidates('win32', 'x64', false).map((item) => item.packageName)
|
||||||
|
).toEqual(['opencode-windows-x64', 'opencode-windows-x64-baseline']);
|
||||||
|
expect(
|
||||||
|
getOpenCodeRuntimePlatformCandidates('win32', 'arm64', false).map((item) => item.packageName)
|
||||||
|
).toEqual(['opencode-windows-arm64']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails npm integrity mismatches', () => {
|
||||||
|
const payload = Buffer.from('actual package');
|
||||||
|
const wrongHash = createHash('sha512').update('different package').digest('base64');
|
||||||
|
|
||||||
|
expect(() => verifyOpenCodeRuntimePackageIntegrity(payload, `sha512-${wrongHash}`)).toThrow(
|
||||||
|
'integrity check failed'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts only the expected OpenCode binary from the package tarball', () => {
|
||||||
|
const tarball = createTarball([
|
||||||
|
{ name: 'package/bin/not-opencode', data: 'wrong' },
|
||||||
|
{
|
||||||
|
name: process.platform === 'win32' ? 'package/bin/opencode.exe' : 'package/bin/opencode',
|
||||||
|
data: 'right',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(extractOpenCodeRuntimeBinaryFromTarball(tarball).toString()).toBe('right');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects tar path traversal before extraction', () => {
|
||||||
|
const tarball = createTarball([
|
||||||
|
{ name: '../opencode', data: 'unsafe' },
|
||||||
|
{
|
||||||
|
name: process.platform === 'win32' ? 'package/bin/opencode.exe' : 'package/bin/opencode',
|
||||||
|
data: 'right',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(() => extractOpenCodeRuntimeBinaryFromTarball(tarball)).toThrow(
|
||||||
|
'Unsafe OpenCode package tar entry'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,11 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
'runtime status --json --provider opencode',
|
'runtime status --json --provider opencode',
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
execCliMock.mock.calls
|
||||||
|
.filter((call) => call[1].join(' ').startsWith('runtime status --json --provider '))
|
||||||
|
.map((call) => call[2]?.maxBuffer)
|
||||||
|
).toEqual([8 * 1024 * 1024, 8 * 1024 * 1024, 8 * 1024 * 1024, 8 * 1024 * 1024]);
|
||||||
expect(providers.map((provider) => provider.providerId)).toEqual([
|
expect(providers.map((provider) => provider.providerId)).toEqual([
|
||||||
'anthropic',
|
'anthropic',
|
||||||
'codex',
|
'codex',
|
||||||
|
|
@ -448,6 +453,12 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
upgrade: false,
|
upgrade: false,
|
||||||
source: 'anthropic-models-api',
|
source: 'anthropic-models-api',
|
||||||
badgeLabel: 'Opus 4.8',
|
badgeLabel: 'Opus 4.8',
|
||||||
|
metadata: {
|
||||||
|
cost: { input: 0, output: 0 },
|
||||||
|
context: 200000,
|
||||||
|
limits: { context: 200000, output: 32000 },
|
||||||
|
free: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'opus[1m]',
|
id: 'opus[1m]',
|
||||||
|
|
@ -539,6 +550,12 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
source: 'anthropic-models-api',
|
source: 'anthropic-models-api',
|
||||||
badgeLabel: 'Opus 4.8',
|
badgeLabel: 'Opus 4.8',
|
||||||
|
metadata: {
|
||||||
|
cost: { input: 0, output: 0 },
|
||||||
|
context: 200000,
|
||||||
|
limits: { context: 200000, output: 32000 },
|
||||||
|
free: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
launchModel: 'opus[1m]',
|
launchModel: 'opus[1m]',
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,54 @@ describe('ProviderConnectionService', () => {
|
||||||
expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not decrypt stored Anthropic keys when metadata-only env building is requested', async () => {
|
||||||
|
const lookupPreferred = vi.fn().mockResolvedValue({
|
||||||
|
envVarName: 'ANTHROPIC_API_KEY',
|
||||||
|
value: 'stored-key',
|
||||||
|
});
|
||||||
|
const { ProviderConnectionService } =
|
||||||
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
||||||
|
const service = new ProviderConnectionService(
|
||||||
|
{
|
||||||
|
lookupPreferred,
|
||||||
|
} as never,
|
||||||
|
{
|
||||||
|
getConfig: () => createConfig('api_key'),
|
||||||
|
} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await service.applyConfiguredConnectionEnv({}, 'anthropic', undefined, {
|
||||||
|
allowStoredApiKeyDecryption: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lookupPreferred).not.toHaveBeenCalled();
|
||||||
|
expect(result.ANTHROPIC_API_KEY).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects stored Gemini API keys for runtime launches', async () => {
|
||||||
|
const lookupPreferred = vi.fn().mockResolvedValue({
|
||||||
|
envVarName: 'GEMINI_API_KEY',
|
||||||
|
value: 'gemini-stored-key',
|
||||||
|
});
|
||||||
|
const { ProviderConnectionService } =
|
||||||
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
||||||
|
const service = new ProviderConnectionService(
|
||||||
|
{
|
||||||
|
lookupPreferred,
|
||||||
|
} as never,
|
||||||
|
{
|
||||||
|
getConfig: () => createConfig('auto'),
|
||||||
|
} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await service.applyConfiguredConnectionEnv({}, 'gemini');
|
||||||
|
|
||||||
|
expect(lookupPreferred).toHaveBeenCalledWith('GEMINI_API_KEY');
|
||||||
|
expect(result.GEMINI_API_KEY).toBe('gemini-stored-key');
|
||||||
|
});
|
||||||
|
|
||||||
it('reports a missing Anthropic API key when api_key mode is selected', async () => {
|
it('reports a missing Anthropic API key when api_key mode is selected', async () => {
|
||||||
const { ProviderConnectionService } =
|
const { ProviderConnectionService } =
|
||||||
await import('@main/services/runtime/ProviderConnectionService');
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ const applyConfiguredConnectionEnvMock = vi.fn();
|
||||||
const applyAllConfiguredConnectionEnvMock = vi.fn();
|
const applyAllConfiguredConnectionEnvMock = vi.fn();
|
||||||
const getConfiguredConnectionIssuesMock = vi.fn();
|
const getConfiguredConnectionIssuesMock = vi.fn();
|
||||||
const getConfiguredConnectionLaunchArgsMock = vi.fn();
|
const getConfiguredConnectionLaunchArgsMock = vi.fn();
|
||||||
|
const resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock = vi.fn();
|
||||||
|
|
||||||
vi.mock('@main/utils/cliEnv', () => ({
|
vi.mock('@main/utils/cliEnv', () => ({
|
||||||
buildEnrichedEnv: (...args: Parameters<typeof buildEnrichedEnvMock>) => buildEnrichedEnvMock(...args),
|
buildEnrichedEnv: (...args: Parameters<typeof buildEnrichedEnvMock>) =>
|
||||||
|
buildEnrichedEnvMock(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@main/utils/shellEnv', () => ({
|
vi.mock('@main/utils/shellEnv', () => ({
|
||||||
|
|
@ -35,22 +37,31 @@ vi.mock('../../../../src/main/services/infrastructure/ConfigManager', () => ({
|
||||||
|
|
||||||
vi.mock('../../../../src/main/services/runtime/ProviderConnectionService', () => ({
|
vi.mock('../../../../src/main/services/runtime/ProviderConnectionService', () => ({
|
||||||
providerConnectionService: {
|
providerConnectionService: {
|
||||||
augmentConfiguredConnectionEnv: (...args: Parameters<typeof augmentConfiguredConnectionEnvMock>) =>
|
augmentConfiguredConnectionEnv: (
|
||||||
augmentConfiguredConnectionEnvMock(...args),
|
...args: Parameters<typeof augmentConfiguredConnectionEnvMock>
|
||||||
augmentAllConfiguredConnectionEnv: (...args: Parameters<typeof augmentAllConfiguredConnectionEnvMock>) =>
|
) => augmentConfiguredConnectionEnvMock(...args),
|
||||||
augmentAllConfiguredConnectionEnvMock(...args),
|
augmentAllConfiguredConnectionEnv: (
|
||||||
|
...args: Parameters<typeof augmentAllConfiguredConnectionEnvMock>
|
||||||
|
) => augmentAllConfiguredConnectionEnvMock(...args),
|
||||||
applyConfiguredConnectionEnv: (...args: Parameters<typeof applyConfiguredConnectionEnvMock>) =>
|
applyConfiguredConnectionEnv: (...args: Parameters<typeof applyConfiguredConnectionEnvMock>) =>
|
||||||
applyConfiguredConnectionEnvMock(...args),
|
applyConfiguredConnectionEnvMock(...args),
|
||||||
applyAllConfiguredConnectionEnv: (...args: Parameters<typeof applyAllConfiguredConnectionEnvMock>) =>
|
applyAllConfiguredConnectionEnv: (
|
||||||
applyAllConfiguredConnectionEnvMock(...args),
|
...args: Parameters<typeof applyAllConfiguredConnectionEnvMock>
|
||||||
|
) => applyAllConfiguredConnectionEnvMock(...args),
|
||||||
getConfiguredConnectionLaunchArgs: (
|
getConfiguredConnectionLaunchArgs: (
|
||||||
...args: Parameters<typeof getConfiguredConnectionLaunchArgsMock>
|
...args: Parameters<typeof getConfiguredConnectionLaunchArgsMock>
|
||||||
) => getConfiguredConnectionLaunchArgsMock(...args),
|
) => getConfiguredConnectionLaunchArgsMock(...args),
|
||||||
getConfiguredConnectionIssues: (...args: Parameters<typeof getConfiguredConnectionIssuesMock>) =>
|
getConfiguredConnectionIssues: (
|
||||||
getConfiguredConnectionIssuesMock(...args),
|
...args: Parameters<typeof getConfiguredConnectionIssuesMock>
|
||||||
|
) => getConfiguredConnectionIssuesMock(...args),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../src/main/services/infrastructure/OpenCodeRuntimeInstallerService', () => ({
|
||||||
|
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath: () =>
|
||||||
|
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('buildProviderAwareCliEnv', () => {
|
describe('buildProviderAwareCliEnv', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
|
@ -76,6 +87,7 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
);
|
);
|
||||||
getConfiguredConnectionLaunchArgsMock.mockResolvedValue([]);
|
getConfiguredConnectionLaunchArgsMock.mockResolvedValue([]);
|
||||||
getConfiguredConnectionIssuesMock.mockResolvedValue({});
|
getConfiguredConnectionIssuesMock.mockResolvedValue({});
|
||||||
|
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock.mockResolvedValue(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds provider-pinned CLI env and returns provider-specific issues', async () => {
|
it('builds provider-pinned CLI env and returns provider-specific issues', async () => {
|
||||||
|
|
@ -83,9 +95,8 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
anthropic: 'missing key',
|
anthropic: 'missing key',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { buildProviderAwareCliEnv } = await import(
|
const { buildProviderAwareCliEnv } =
|
||||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
);
|
|
||||||
const result = await buildProviderAwareCliEnv({
|
const result = await buildProviderAwareCliEnv({
|
||||||
binaryPath: '/mock/claude',
|
binaryPath: '/mock/claude',
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
|
|
@ -112,10 +123,27 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
expect(result.providerArgs).toEqual([]);
|
expect(result.providerArgs).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds shared env for generic CLI launches when no provider is specified', async () => {
|
it('passes metadata-only stored API key access through provider env building', async () => {
|
||||||
const { buildProviderAwareCliEnv } = await import(
|
const { buildProviderAwareCliEnv } =
|
||||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
|
await buildProviderAwareCliEnv({
|
||||||
|
providerId: 'anthropic',
|
||||||
|
allowStoredApiKeyDecryption: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
CLAUDE_CODE_ENTRY_PROVIDER: 'anthropic',
|
||||||
|
}),
|
||||||
|
'anthropic',
|
||||||
|
undefined,
|
||||||
|
{ allowStoredApiKeyDecryption: false }
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds shared env for generic CLI launches when no provider is specified', async () => {
|
||||||
|
const { buildProviderAwareCliEnv } =
|
||||||
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
const result = await buildProviderAwareCliEnv();
|
const result = await buildProviderAwareCliEnv();
|
||||||
|
|
||||||
expect(applyAllConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
expect(applyAllConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
||||||
|
|
@ -140,9 +168,8 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
PATH: '/usr/bin',
|
PATH: '/usr/bin',
|
||||||
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
||||||
});
|
});
|
||||||
const { buildProviderAwareCliEnv } = await import(
|
const { buildProviderAwareCliEnv } =
|
||||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
);
|
|
||||||
|
|
||||||
const result = await buildProviderAwareCliEnv({
|
const result = await buildProviderAwareCliEnv({
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -155,9 +182,8 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses non-destructive credential augmentation for PTY-style envs', async () => {
|
it('uses non-destructive credential augmentation for PTY-style envs', async () => {
|
||||||
const { buildProviderAwareCliEnv } = await import(
|
const { buildProviderAwareCliEnv } =
|
||||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
);
|
|
||||||
const result = await buildProviderAwareCliEnv({
|
const result = await buildProviderAwareCliEnv({
|
||||||
connectionMode: 'augment',
|
connectionMode: 'augment',
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -176,9 +202,8 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves caller-provided HOME and USERPROFILE overrides', async () => {
|
it('preserves caller-provided HOME and USERPROFILE overrides', async () => {
|
||||||
const { buildProviderAwareCliEnv } = await import(
|
const { buildProviderAwareCliEnv } =
|
||||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
);
|
|
||||||
const result = await buildProviderAwareCliEnv({
|
const result = await buildProviderAwareCliEnv({
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -201,9 +226,8 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves explicit backend overrides passed by the caller', async () => {
|
it('preserves explicit backend overrides passed by the caller', async () => {
|
||||||
const { buildProviderAwareCliEnv } = await import(
|
const { buildProviderAwareCliEnv } =
|
||||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
);
|
|
||||||
const result = await buildProviderAwareCliEnv({
|
const result = await buildProviderAwareCliEnv({
|
||||||
connectionMode: 'augment',
|
connectionMode: 'augment',
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -227,9 +251,8 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
PATH: '/usr/bin',
|
PATH: '/usr/bin',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { buildProviderAwareCliEnv } = await import(
|
const { buildProviderAwareCliEnv } =
|
||||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
);
|
|
||||||
const result = await buildProviderAwareCliEnv({
|
const result = await buildProviderAwareCliEnv({
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
});
|
});
|
||||||
|
|
@ -251,9 +274,8 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
'{"codex":{"forced_login_method":"chatgpt"}}',
|
'{"codex":{"forced_login_method":"chatgpt"}}',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { buildProviderAwareCliEnv } = await import(
|
const { buildProviderAwareCliEnv } =
|
||||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
);
|
|
||||||
const result = await buildProviderAwareCliEnv({
|
const result = await buildProviderAwareCliEnv({
|
||||||
binaryPath: '/mock/claude-multimodel',
|
binaryPath: '/mock/claude-multimodel',
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
|
|
@ -272,4 +294,42 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
'{"codex":{"forced_login_method":"chatgpt"}}',
|
'{"codex":{"forced_login_method":"chatgpt"}}',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('injects the verified app-managed OpenCode binary for OpenCode launches', async () => {
|
||||||
|
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock.mockResolvedValue(
|
||||||
|
'/Users/tester/App Support/runtimes/opencode/current/opencode'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { buildProviderAwareCliEnv } =
|
||||||
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
|
const result = await buildProviderAwareCliEnv({
|
||||||
|
providerId: 'opencode',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH:
|
||||||
|
'/Users/tester/App Support/runtimes/opencode/current/opencode',
|
||||||
|
}),
|
||||||
|
'opencode',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(
|
||||||
|
'/Users/tester/App Support/runtimes/opencode/current/opencode'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not inject the app-managed OpenCode binary into non-OpenCode provider launches', async () => {
|
||||||
|
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock.mockResolvedValue(
|
||||||
|
'/Users/tester/App Support/runtimes/opencode/current/opencode'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { buildProviderAwareCliEnv } =
|
||||||
|
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||||
|
const result = await buildProviderAwareCliEnv({
|
||||||
|
providerId: 'anthropic',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -308,9 +308,7 @@ describe('OpenCodeReadinessBridge', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recovers accepted OpenCode sendMessage after bridge timeout through commandStatus when enabled', async () => {
|
it('recovers accepted OpenCode sendMessage after bridge timeout through commandStatus by default', async () => {
|
||||||
const previous = process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY;
|
|
||||||
process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY = '1';
|
|
||||||
const executor = fakeSequenceExecutor([
|
const executor = fakeSequenceExecutor([
|
||||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||||
bridgeCommandSuccess({
|
bridgeCommandSuccess({
|
||||||
|
|
@ -328,7 +326,6 @@ describe('OpenCodeReadinessBridge', () => {
|
||||||
]);
|
]);
|
||||||
const bridge = new OpenCodeReadinessBridge(executor);
|
const bridge = new OpenCodeReadinessBridge(executor);
|
||||||
|
|
||||||
try {
|
|
||||||
await expect(
|
await expect(
|
||||||
bridge.sendOpenCodeTeamMessage({
|
bridge.sendOpenCodeTeamMessage({
|
||||||
teamId: 'team-a',
|
teamId: 'team-a',
|
||||||
|
|
@ -349,13 +346,6 @@ describe('OpenCodeReadinessBridge', () => {
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
if (previous === undefined) {
|
|
||||||
delete process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY;
|
|
||||||
} else {
|
|
||||||
process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY = previous;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||||
const sendOptions = executor.execute.mock.calls[0]?.[2] as { requestId?: string } | undefined;
|
const sendOptions = executor.execute.mock.calls[0]?.[2] as { requestId?: string } | undefined;
|
||||||
|
|
@ -375,10 +365,7 @@ describe('OpenCodeReadinessBridge', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not query commandStatus for non-timeout OpenCode sendMessage failures', async () => {
|
it('does not query commandStatus for non-timeout OpenCode sendMessage failures', async () => {
|
||||||
await withCommandStatusRecoveryEnabled(async () => {
|
const executor = fakeExecutor(bridgeFailure('provider_error', 'OpenCode send failed', []));
|
||||||
const executor = fakeExecutor(
|
|
||||||
bridgeFailure('provider_error', 'OpenCode send failed', [])
|
|
||||||
);
|
|
||||||
const bridge = new OpenCodeReadinessBridge(executor);
|
const bridge = new OpenCodeReadinessBridge(executor);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|
@ -404,10 +391,8 @@ describe('OpenCodeReadinessBridge', () => {
|
||||||
|
|
||||||
expect(executor.execute).toHaveBeenCalledOnce();
|
expect(executor.execute).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the old send failure path when timeout commandStatus is unknown', async () => {
|
it('keeps the timeout failure path when timeout commandStatus is unknown', async () => {
|
||||||
await withCommandStatusRecoveryEnabled(async () => {
|
|
||||||
const executor = fakeSequenceExecutor([
|
const executor = fakeSequenceExecutor([
|
||||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||||
bridgeCommandSuccess({
|
bridgeCommandSuccess({
|
||||||
|
|
@ -446,10 +431,8 @@ describe('OpenCodeReadinessBridge', () => {
|
||||||
|
|
||||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the old send failure path when timeout commandStatus is unavailable', async () => {
|
it('keeps the timeout failure path when timeout commandStatus is unavailable', async () => {
|
||||||
await withCommandStatusRecoveryEnabled(async () => {
|
|
||||||
const executor = fakeSequenceExecutor([
|
const executor = fakeSequenceExecutor([
|
||||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||||
bridgeFailure('timeout', 'OpenCode commandStatus timed out', []),
|
bridgeFailure('timeout', 'OpenCode commandStatus timed out', []),
|
||||||
|
|
@ -479,10 +462,8 @@ describe('OpenCodeReadinessBridge', () => {
|
||||||
|
|
||||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the old send failure path when timeout commandStatus reports precondition mismatch', async () => {
|
it('keeps the timeout failure path when timeout commandStatus reports precondition mismatch', async () => {
|
||||||
await withCommandStatusRecoveryEnabled(async () => {
|
|
||||||
const executor = fakeSequenceExecutor([
|
const executor = fakeSequenceExecutor([
|
||||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||||
bridgeCommandSuccess({
|
bridgeCommandSuccess({
|
||||||
|
|
@ -521,7 +502,6 @@ describe('OpenCodeReadinessBridge', () => {
|
||||||
|
|
||||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('routes state-changing launch commands through the guarded command service when configured', async () => {
|
it('routes state-changing launch commands through the guarded command service when configured', async () => {
|
||||||
const executor = fakeExecutor(
|
const executor = fakeExecutor(
|
||||||
|
|
@ -610,20 +590,6 @@ function fakeSequenceExecutor(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withCommandStatusRecoveryEnabled<T>(callback: () => Promise<T>): Promise<T> {
|
|
||||||
const previous = process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY;
|
|
||||||
process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY = '1';
|
|
||||||
try {
|
|
||||||
return await callback();
|
|
||||||
} finally {
|
|
||||||
if (previous === undefined) {
|
|
||||||
delete process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY;
|
|
||||||
} else {
|
|
||||||
process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY = previous;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bridgeSuccess(
|
function bridgeSuccess(
|
||||||
data: OpenCodeTeamLaunchReadiness
|
data: OpenCodeTeamLaunchReadiness
|
||||||
): OpenCodeBridgeSuccess<OpenCodeTeamLaunchReadiness> {
|
): OpenCodeBridgeSuccess<OpenCodeTeamLaunchReadiness> {
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,29 @@ describe('TeamLaunchFailureArtifactPack', () => {
|
||||||
).toBe('provider_quota');
|
).toBe('provider_quota');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('classifies Claude Code workspace trust failures separately', () => {
|
||||||
|
const reason =
|
||||||
|
'Teammate "Gayani" cannot start in headless process runtime because workspace trust is not accepted for "C:\\Users\\vilok\\OneDrive\\Desktop\\Safar 0.1". Open that workspace once interactively and accept trust, then launch the team again.';
|
||||||
|
|
||||||
|
const classification = classifyLaunchFailureArtifact({
|
||||||
|
teamName: 'artifact-team',
|
||||||
|
runId: 'run-workspace-trust',
|
||||||
|
reason: 'Deterministic bootstrap failed',
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
Gayani: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
hardFailureReason: reason,
|
||||||
|
updatedAt: '2026-05-12T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
progressTraceLines: [reason],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(classification.code).toBe('workspace_trust_required');
|
||||||
|
expect(classification.evidence.join('\n')).toContain('workspace trust is not accepted');
|
||||||
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
name: 'stdin warning',
|
name: 'stdin warning',
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,53 @@ describe('TeamMemberLogsFinder', () => {
|
||||||
expect(projectResolver.getContext).toHaveBeenCalledTimes(2);
|
expect(projectResolver.getContext).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('dedupes concurrent forceRefresh log source discovery for the same team', async () => {
|
||||||
|
const teamName = 'dedupe-force-refresh-context-team';
|
||||||
|
let resolveContext!: (value: unknown) => void;
|
||||||
|
const contextPromise = new Promise((resolve) => {
|
||||||
|
resolveContext = resolve;
|
||||||
|
});
|
||||||
|
const projectResolver = {
|
||||||
|
getContext: vi.fn(() => contextPromise),
|
||||||
|
getLiveBaseContext: vi.fn(),
|
||||||
|
};
|
||||||
|
const inboxReader = { listInboxNames: vi.fn(async () => []) };
|
||||||
|
const membersMetaStore = { getMembers: vi.fn(async () => []) };
|
||||||
|
const finder = new TeamMemberLogsFinder(
|
||||||
|
undefined,
|
||||||
|
inboxReader as never,
|
||||||
|
membersMetaStore as never,
|
||||||
|
projectResolver as never
|
||||||
|
);
|
||||||
|
|
||||||
|
const first = finder.getLogSourceWatchContext(teamName, { forceRefresh: true });
|
||||||
|
const second = finder.getLogSourceWatchContext(teamName, { forceRefresh: true });
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(projectResolver.getContext).toHaveBeenCalledTimes(1);
|
||||||
|
resolveContext({
|
||||||
|
projectDir: '/tmp/project',
|
||||||
|
projectId: 'project',
|
||||||
|
sessionIds: ['session-1'],
|
||||||
|
config: { name: teamName, projectPath: '/repo', members: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(Promise.all([first, second])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
projectDir: '/tmp/project',
|
||||||
|
projectPath: '/repo',
|
||||||
|
leadSessionId: undefined,
|
||||||
|
sessionIds: ['session-1'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectDir: '/tmp/project',
|
||||||
|
projectPath: '/repo',
|
||||||
|
leadSessionId: undefined,
|
||||||
|
sessionIds: ['session-1'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns subagent logs for a member and lead session for team-lead', async () => {
|
it('returns subagent logs for a member and lead session for team-lead', async () => {
|
||||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-'));
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-'));
|
||||||
setClaudeBasePathOverride(tmpDir);
|
setClaudeBasePathOverride(tmpDir);
|
||||||
|
|
@ -531,6 +578,186 @@ describe('TeamMemberLogsFinder', () => {
|
||||||
expect(refs.some((ref) => ref.memberName === 'Tom')).toBe(false);
|
expect(refs.some((ref) => ref.memberName === 'Tom')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not leak old same-workspace subagent logs into a newly created team', async () => {
|
||||||
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-scope-'));
|
||||||
|
setClaudeBasePathOverride(tmpDir);
|
||||||
|
|
||||||
|
const teamName = 'aurora-room-scope';
|
||||||
|
const projectPath = '/Users/test/shared-workspace';
|
||||||
|
const projectId = '-Users-test-shared-workspace';
|
||||||
|
const leadSessionId = 'fresh-lead-session';
|
||||||
|
const unrelatedSessionId = 'old-other-team-session';
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await fs.mkdir(path.join(tmpDir, 'teams', teamName), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tmpDir, 'teams', teamName, 'config.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: teamName,
|
||||||
|
projectPath,
|
||||||
|
leadSessionId,
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead' },
|
||||||
|
{ name: 'Alice', agentType: 'general-purpose' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectRoot = path.join(tmpDir, 'projects', projectId);
|
||||||
|
const currentSubagentsDir = path.join(projectRoot, leadSessionId, 'subagents');
|
||||||
|
const unrelatedSubagentsDir = path.join(projectRoot, unrelatedSessionId, 'subagents');
|
||||||
|
await fs.mkdir(currentSubagentsDir, { recursive: true });
|
||||||
|
await fs.mkdir(unrelatedSubagentsDir, { recursive: true });
|
||||||
|
|
||||||
|
const leadPath = path.join(projectRoot, `${leadSessionId}.jsonl`);
|
||||||
|
await fs.writeFile(
|
||||||
|
leadPath,
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: now.toISOString(),
|
||||||
|
type: 'user',
|
||||||
|
message: { role: 'user', content: `Lead for team "${teamName}" (${teamName})` },
|
||||||
|
}) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentAlicePath = path.join(currentSubagentsDir, 'agent-alice.jsonl');
|
||||||
|
await fs.writeFile(
|
||||||
|
currentAlicePath,
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: now.toISOString(),
|
||||||
|
type: 'user',
|
||||||
|
message: {
|
||||||
|
role: 'user',
|
||||||
|
content: `You are Alice, a developer on team "${teamName}" (${teamName}).`,
|
||||||
|
},
|
||||||
|
}) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const unrelatedAlicePath = path.join(unrelatedSubagentsDir, 'agent-alice.jsonl');
|
||||||
|
await fs.writeFile(
|
||||||
|
unrelatedAlicePath,
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: now.toISOString(),
|
||||||
|
type: 'user',
|
||||||
|
message: {
|
||||||
|
role: 'user',
|
||||||
|
content: 'You are Alice, a developer on team "old-team" (old-team).',
|
||||||
|
},
|
||||||
|
}) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const refs = await new TeamMemberLogsFinder().findRecentMemberLogFileRefsByMember(
|
||||||
|
teamName,
|
||||||
|
['team-lead', 'Alice'],
|
||||||
|
{ forceRefresh: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(refs).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ memberName: 'team-lead', filePath: leadPath }),
|
||||||
|
expect.objectContaining({ memberName: 'Alice', filePath: currentAlicePath }),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect(refs.some((ref) => ref.filePath === unrelatedAlicePath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can skip untracked team subagent session discovery for graph previews', async () => {
|
||||||
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-preview-scope-'));
|
||||||
|
setClaudeBasePathOverride(tmpDir);
|
||||||
|
|
||||||
|
const teamName = 'preview-known-session-scope';
|
||||||
|
const projectPath = '/Users/test/preview-known-session-scope';
|
||||||
|
const projectId = '-Users-test-preview-known-session-scope';
|
||||||
|
const leadSessionId = 'known-lead-session';
|
||||||
|
const untrackedSessionId = 'team-subagent-only-session';
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await fs.mkdir(path.join(tmpDir, 'teams', teamName), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tmpDir, 'teams', teamName, 'config.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: teamName,
|
||||||
|
projectPath,
|
||||||
|
leadSessionId,
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead' },
|
||||||
|
{ name: 'Alice', agentType: 'general-purpose' },
|
||||||
|
{ name: 'Bob', agentType: 'general-purpose' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectRoot = path.join(tmpDir, 'projects', projectId);
|
||||||
|
const knownSubagentsDir = path.join(projectRoot, leadSessionId, 'subagents');
|
||||||
|
const untrackedSubagentsDir = path.join(projectRoot, untrackedSessionId, 'subagents');
|
||||||
|
await fs.mkdir(knownSubagentsDir, { recursive: true });
|
||||||
|
await fs.mkdir(untrackedSubagentsDir, { recursive: true });
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(projectRoot, `${leadSessionId}.jsonl`),
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: now.toISOString(),
|
||||||
|
type: 'user',
|
||||||
|
message: { role: 'user', content: `Lead for team "${teamName}" (${teamName})` },
|
||||||
|
}) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const knownAlicePath = path.join(knownSubagentsDir, 'agent-alice.jsonl');
|
||||||
|
await fs.writeFile(
|
||||||
|
knownAlicePath,
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: now.toISOString(),
|
||||||
|
type: 'user',
|
||||||
|
message: {
|
||||||
|
role: 'user',
|
||||||
|
content: `You are Alice, a developer on team "${teamName}" (${teamName}).`,
|
||||||
|
},
|
||||||
|
}) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const untrackedBobPath = path.join(untrackedSubagentsDir, 'agent-bob.jsonl');
|
||||||
|
await fs.writeFile(
|
||||||
|
untrackedBobPath,
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: now.toISOString(),
|
||||||
|
type: 'user',
|
||||||
|
message: {
|
||||||
|
role: 'user',
|
||||||
|
content: `You are Bob, a developer on team "${teamName}" (${teamName}).`,
|
||||||
|
},
|
||||||
|
}) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const finder = new TeamMemberLogsFinder();
|
||||||
|
const fastRefs = await finder.findRecentMemberLogFileRefsByMember(teamName, ['Alice', 'Bob'], {
|
||||||
|
forceRefresh: true,
|
||||||
|
includeTeamSubagentSessionDiscovery: false,
|
||||||
|
});
|
||||||
|
expect(fastRefs.some((ref) => ref.filePath === knownAlicePath)).toBe(true);
|
||||||
|
expect(fastRefs.some((ref) => ref.filePath === untrackedBobPath)).toBe(false);
|
||||||
|
|
||||||
|
const fullRefs = await finder.findRecentMemberLogFileRefsByMember(teamName, ['Alice', 'Bob'], {
|
||||||
|
forceRefresh: true,
|
||||||
|
});
|
||||||
|
expect(fullRefs.some((ref) => ref.filePath === knownAlicePath)).toBe(true);
|
||||||
|
expect(fullRefs.some((ref) => ref.filePath === untrackedBobPath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('applies recent-ref object options to discovery, lead refs, metadata, and requested-member attribution', async () => {
|
it('applies recent-ref object options to discovery, lead refs, metadata, and requested-member attribution', async () => {
|
||||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-'));
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-'));
|
||||||
setClaudeBasePathOverride(tmpDir);
|
setClaudeBasePathOverride(tmpDir);
|
||||||
|
|
|
||||||
|
|
@ -703,6 +703,87 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('team launch notifications', () => {
|
describe('team launch notifications', () => {
|
||||||
|
it('fires team launched when the last pending teammate joins after ready', async () => {
|
||||||
|
const { NotificationManager } =
|
||||||
|
await import('@main/services/infrastructure/NotificationManager');
|
||||||
|
const addTeamNotification = vi.fn(async (_payload: unknown) => undefined);
|
||||||
|
NotificationManager.setInstance({ addTeamNotification } as never);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const run = {
|
||||||
|
runId: 'run-late-all-joined',
|
||||||
|
teamName: 'late-all-joined-team',
|
||||||
|
isLaunch: true,
|
||||||
|
provisioningComplete: true,
|
||||||
|
processKilled: false,
|
||||||
|
cancelRequested: false,
|
||||||
|
progress: { state: 'ready' },
|
||||||
|
request: {
|
||||||
|
cwd: tempClaudeRoot,
|
||||||
|
displayName: 'late-all-joined-team',
|
||||||
|
},
|
||||||
|
expectedMembers: ['alice', 'bob'],
|
||||||
|
allEffectiveMembers: [{ name: 'alice' }, { name: 'bob' }],
|
||||||
|
memberSpawnStatuses: new Map([
|
||||||
|
[
|
||||||
|
'alice',
|
||||||
|
createMemberSpawnStatusEntry({
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bob',
|
||||||
|
createMemberSpawnStatusEntry({
|
||||||
|
status: 'waiting',
|
||||||
|
launchState: 'runtime_pending_bootstrap',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
(svc as any).runs.set(run.runId, run);
|
||||||
|
(svc as any).aliveRunByTeam.set(run.teamName, run.runId);
|
||||||
|
|
||||||
|
(svc as any).emitMemberSpawnChange(run, 'bob');
|
||||||
|
expect(addTeamNotification).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
run.memberSpawnStatuses.set(
|
||||||
|
'bob',
|
||||||
|
createMemberSpawnStatusEntry({
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
(svc as any).emitMemberSpawnChange(run, 'bob');
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(addTeamNotification).toHaveBeenCalledTimes(1);
|
||||||
|
expect(addTeamNotification).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
teamEventType: 'team_launched',
|
||||||
|
teamName: 'late-all-joined-team',
|
||||||
|
dedupeKey: 'team_launched:late-all-joined-team:run-late-all-joined',
|
||||||
|
body:
|
||||||
|
'Team "late-all-joined-team" has been launched - all 2 teammates joined and are ready for tasks.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
(svc as any).emitMemberSpawnChange(run, 'bob');
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(addTeamNotification).toHaveBeenCalledTimes(1);
|
||||||
|
} finally {
|
||||||
|
NotificationManager.resetInstance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('does not fire incomplete notification for pending-only teammates still joining', async () => {
|
it('does not fire incomplete notification for pending-only teammates still joining', async () => {
|
||||||
const { NotificationManager } =
|
const { NotificationManager } =
|
||||||
await import('@main/services/infrastructure/NotificationManager');
|
await import('@main/services/infrastructure/NotificationManager');
|
||||||
|
|
@ -17476,6 +17557,58 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reports workspace trust failures with a specific deterministic bootstrap title', () => {
|
||||||
|
const reason =
|
||||||
|
'Teammate "Gayani" cannot start in headless process runtime because workspace trust is not accepted for "C:\\Users\\vilok\\OneDrive\\Desktop\\Safar 0.1". Open that workspace once interactively and accept trust, then launch the team again.';
|
||||||
|
const progressUpdates: any[] = [];
|
||||||
|
const run = createMemberSpawnRun({
|
||||||
|
runId: 'run-workspace-trust-bootstrap',
|
||||||
|
teamName: 'workspace-trust-bootstrap-team',
|
||||||
|
expectedMembers: ['Gayani'],
|
||||||
|
});
|
||||||
|
Object.assign(run, {
|
||||||
|
cancelRequested: false,
|
||||||
|
isLaunch: false,
|
||||||
|
lastDeterministicBootstrapSeq: 0,
|
||||||
|
progress: {
|
||||||
|
runId: run.runId,
|
||||||
|
teamName: run.teamName,
|
||||||
|
state: 'assembling',
|
||||||
|
message: 'Spawning teammate runtimes',
|
||||||
|
startedAt: '2026-05-12T10:00:00.000Z',
|
||||||
|
updatedAt: '2026-05-12T10:00:00.000Z',
|
||||||
|
},
|
||||||
|
onProgress: (progress: any) => {
|
||||||
|
progressUpdates.push(progress);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
vi.spyOn(svc as any, 'persistLaunchStateSnapshot').mockResolvedValue(null);
|
||||||
|
vi.spyOn(svc as any, 'cleanupRun').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const handled = (svc as any).handleDeterministicBootstrapEvent(run, {
|
||||||
|
type: 'system',
|
||||||
|
subtype: 'team_bootstrap',
|
||||||
|
event: 'failed',
|
||||||
|
reason,
|
||||||
|
run_id: run.runId,
|
||||||
|
team_name: run.teamName,
|
||||||
|
seq: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handled).toBe(true);
|
||||||
|
expect(progressUpdates.at(-1)).toMatchObject({
|
||||||
|
state: 'failed',
|
||||||
|
message: 'Workspace trust required',
|
||||||
|
error: reason,
|
||||||
|
});
|
||||||
|
expect(run.memberSpawnStatuses.get('Gayani')).toMatchObject({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
hardFailureReason: reason,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('clears stale failed_to_start state when live runtime metadata proves the teammate is alive', async () => {
|
it('clears stale failed_to_start state when live runtime metadata proves the teammate is alive', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
(svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn(
|
(svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn(
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,11 @@ describe('TeamTranscriptProjectResolver', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await fs.writeFile(jsonlPath, `${lines.map((line) => JSON.stringify(line)).join('\n')}\n`, 'utf8');
|
await fs.writeFile(
|
||||||
|
jsonlPath,
|
||||||
|
`${lines.map((line) => JSON.stringify(line)).join('\n')}\n`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
return { projectDir, jsonlPath };
|
return { projectDir, jsonlPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,7 +261,9 @@ describe('TeamTranscriptProjectResolver', () => {
|
||||||
const staleProjectPath = '/Users/test/hookplex';
|
const staleProjectPath = '/Users/test/hookplex';
|
||||||
const repairedProjectPath = '/Users/test/plugin-kit-ai';
|
const repairedProjectPath = '/Users/test/plugin-kit-ai';
|
||||||
const historicalSessionId = 'lead-old';
|
const historicalSessionId = 'lead-old';
|
||||||
await fs.mkdir(path.join(tmpDir!, 'projects', encodePath(staleProjectPath)), { recursive: true });
|
await fs.mkdir(path.join(tmpDir!, 'projects', encodePath(staleProjectPath)), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
const repaired = await createSessionFile(repairedProjectPath, historicalSessionId);
|
const repaired = await createSessionFile(repairedProjectPath, historicalSessionId);
|
||||||
|
|
||||||
await writeTeamConfig(teamName, {
|
await writeTeamConfig(teamName, {
|
||||||
|
|
@ -391,7 +397,9 @@ describe('TeamTranscriptProjectResolver', () => {
|
||||||
expect(warnSpy.mock.calls).toEqual(
|
expect(warnSpy.mock.calls).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('Transcript project resolution ambiguous across exact-session candidates'),
|
expect.stringContaining(
|
||||||
|
'Transcript project resolution ambiguous across exact-session candidates'
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
@ -427,7 +435,9 @@ describe('TeamTranscriptProjectResolver', () => {
|
||||||
expect(warnSpy.mock.calls).toEqual(
|
expect(warnSpy.mock.calls).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('Transcript project resolution ambiguous across exact-session candidates'),
|
expect.stringContaining(
|
||||||
|
'Transcript project resolution ambiguous across exact-session candidates'
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
@ -510,4 +520,61 @@ describe('TeamTranscriptProjectResolver', () => {
|
||||||
expect(context?.projectDir).toBe(repaired.projectDir);
|
expect(context?.projectDir).toBe(repaired.projectDir);
|
||||||
expect(context?.config.projectPath).toBe(repairedProjectPath);
|
expect(context?.config.projectPath).toBe(repairedProjectPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('bounds root session discovery by team lifecycle in fast preview context', async () => {
|
||||||
|
await setupClaudeRoot();
|
||||||
|
|
||||||
|
const teamName = 'fast-preview-team';
|
||||||
|
const projectPath = '/Users/test/fast-preview';
|
||||||
|
const createdAt = Date.parse('2026-04-18T12:00:00.000Z');
|
||||||
|
const leadSessionId = 'lead-fast';
|
||||||
|
const lead = await createTeamAwareSessionFile(projectPath, leadSessionId, teamName, 'text');
|
||||||
|
const recent = await createTeamAwareSessionFile(
|
||||||
|
projectPath,
|
||||||
|
'recent-member-session',
|
||||||
|
teamName,
|
||||||
|
'text'
|
||||||
|
);
|
||||||
|
const old = await createTeamAwareSessionFile(
|
||||||
|
projectPath,
|
||||||
|
'old-member-session',
|
||||||
|
teamName,
|
||||||
|
'text'
|
||||||
|
);
|
||||||
|
await fs.utimes(lead.jsonlPath, new Date(createdAt + 60_000), new Date(createdAt + 60_000));
|
||||||
|
await fs.utimes(
|
||||||
|
recent.jsonlPath,
|
||||||
|
new Date(createdAt + 5 * 60_000),
|
||||||
|
new Date(createdAt + 5 * 60_000)
|
||||||
|
);
|
||||||
|
await fs.utimes(
|
||||||
|
old.jsonlPath,
|
||||||
|
new Date(createdAt - 25 * 60 * 60_000),
|
||||||
|
new Date(createdAt - 25 * 60 * 60_000)
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeTeamConfig(teamName, {
|
||||||
|
name: 'Fast Preview Team',
|
||||||
|
createdAt,
|
||||||
|
projectPath,
|
||||||
|
leadSessionId,
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead', joinedAt: createdAt, cwd: projectPath },
|
||||||
|
{ name: 'alice', agentType: 'general-purpose', joinedAt: createdAt + 5 * 60_000 },
|
||||||
|
],
|
||||||
|
} as TeamConfig);
|
||||||
|
|
||||||
|
const resolver = new TeamTranscriptProjectResolver();
|
||||||
|
const fastContext = await resolver.getContext(teamName, {
|
||||||
|
forceRefresh: true,
|
||||||
|
includeTeamSubagentSessionDiscovery: false,
|
||||||
|
});
|
||||||
|
const fullContext = await resolver.getContext(teamName, { forceRefresh: true });
|
||||||
|
|
||||||
|
expect(fastContext?.projectDir).toBe(lead.projectDir);
|
||||||
|
expect(fastContext?.sessionIds).toEqual(expect.arrayContaining([leadSessionId]));
|
||||||
|
expect(fastContext?.sessionIds).toContain('recent-member-session');
|
||||||
|
expect(fastContext?.sessionIds).not.toContain('old-member-session');
|
||||||
|
expect(fullContext?.sessionIds).toContain('old-member-session');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,25 @@ describe('TeamTranscriptSourceLocator', () => {
|
||||||
}) + '\n',
|
}) + '\n',
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
|
const unrelatedSubagentPath = path.join(
|
||||||
|
projectRoot,
|
||||||
|
'unrelated-session-dir',
|
||||||
|
'subagents',
|
||||||
|
'agent-bob.jsonl'
|
||||||
|
);
|
||||||
|
await fs.mkdir(path.dirname(unrelatedSubagentPath), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
unrelatedSubagentPath,
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: '2026-04-15T14:02:02.000Z',
|
||||||
|
type: 'user',
|
||||||
|
message: {
|
||||||
|
role: 'user',
|
||||||
|
content: 'You are bob, a developer on team "other-team" (other-team).',
|
||||||
|
},
|
||||||
|
}) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(projectRoot, leadSessionId, 'subagents', 'agent-worker.jsonl'),
|
path.join(projectRoot, leadSessionId, 'subagents', 'agent-worker.jsonl'),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
@ -126,6 +145,7 @@ describe('TeamTranscriptSourceLocator', () => {
|
||||||
expect(context?.config.projectPath).toBe(projectPath);
|
expect(context?.config.projectPath).toBe(projectPath);
|
||||||
expect(context?.sessionIds).toEqual(expect.arrayContaining([leadSessionId, memberSessionId]));
|
expect(context?.sessionIds).toEqual(expect.arrayContaining([leadSessionId, memberSessionId]));
|
||||||
expect(context?.sessionIds).not.toContain('unrelated-session');
|
expect(context?.sessionIds).not.toContain('unrelated-session');
|
||||||
|
expect(context?.sessionIds).not.toContain('unrelated-session-dir');
|
||||||
expect(transcriptFiles).toEqual(
|
expect(transcriptFiles).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
path.join(projectRoot, `${leadSessionId}.jsonl`),
|
path.join(projectRoot, `${leadSessionId}.jsonl`),
|
||||||
|
|
@ -134,6 +154,7 @@ describe('TeamTranscriptSourceLocator', () => {
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
expect(transcriptFiles).not.toContain(path.join(projectRoot, 'unrelated-session.jsonl'));
|
expect(transcriptFiles).not.toContain(path.join(projectRoot, 'unrelated-session.jsonl'));
|
||||||
|
expect(transcriptFiles).not.toContain(unrelatedSubagentPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the same sorted transcript set across multiple session directories', async () => {
|
it('returns the same sorted transcript set across multiple session directories', async () => {
|
||||||
|
|
@ -193,11 +214,7 @@ describe('TeamTranscriptSourceLocator', () => {
|
||||||
}) + '\n',
|
}) + '\n',
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
await fs.writeFile(
|
await fs.writeFile(path.join(subagentsDir, 'agent-acompact-ignore.jsonl'), '{}\n', 'utf8');
|
||||||
path.join(subagentsDir, 'agent-acompact-ignore.jsonl'),
|
|
||||||
'{}\n',
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
expectedFiles.push(rootTranscript, subagentTranscript);
|
expectedFiles.push(rootTranscript, subagentTranscript);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -437,9 +437,8 @@ describe('CLI status visibility during completed install state', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows an OpenCode download action on the dashboard when the OpenCode CLI is missing', async () => {
|
it('shows an OpenCode install action on the dashboard when the OpenCode CLI is missing', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
const { api } = await import('@renderer/api');
|
|
||||||
storeState.cliInstallerState = 'idle';
|
storeState.cliInstallerState = 'idle';
|
||||||
storeState.cliStatus = createInstalledCliStatus({
|
storeState.cliStatus = createInstalledCliStatus({
|
||||||
flavor: 'agent_teams_orchestrator',
|
flavor: 'agent_teams_orchestrator',
|
||||||
|
|
@ -478,19 +477,137 @@ describe('CLI status visibility during completed install state', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain('OpenCode (75+ LLM providers)');
|
expect(host.textContent).toContain('OpenCode (75+ LLM providers)');
|
||||||
expect(host.textContent).toContain('Download');
|
expect(host.textContent).toContain('Install');
|
||||||
|
|
||||||
const downloadButton = Array.from(host.querySelectorAll('button')).find(
|
const installButton = Array.from(host.querySelectorAll('button')).find(
|
||||||
(button) => button.textContent?.trim() === 'Download'
|
(button) => button.textContent?.trim() === 'Install'
|
||||||
);
|
);
|
||||||
expect(downloadButton).not.toBeUndefined();
|
expect(installButton).not.toBeUndefined();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
downloadButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
installButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(api.openExternal).toHaveBeenCalledWith('https://opencode.ai/download');
|
expect(storeState.installOpenCodeRuntime).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows OpenCode app-managed install progress on the dashboard provider card', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
storeState.cliInstallerState = 'idle';
|
||||||
|
storeState.openCodeRuntimeStatus = {
|
||||||
|
installed: false,
|
||||||
|
source: 'missing',
|
||||||
|
state: 'downloading',
|
||||||
|
progress: {
|
||||||
|
phase: 'downloading',
|
||||||
|
downloadedBytes: 42,
|
||||||
|
totalBytes: 100,
|
||||||
|
percent: 42,
|
||||||
|
detail: 'Downloading OpenCode 42%',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
storeState.openCodeRuntimeStatusLoading = true;
|
||||||
|
storeState.cliStatus = createInstalledCliStatus({
|
||||||
|
flavor: 'agent_teams_orchestrator',
|
||||||
|
displayName: 'Multimodel runtime',
|
||||||
|
supportsSelfUpdate: false,
|
||||||
|
showVersionDetails: false,
|
||||||
|
showBinaryPath: false,
|
||||||
|
authLoggedIn: false,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
providerId: 'opencode',
|
||||||
|
displayName: 'OpenCode (75+ LLM providers)',
|
||||||
|
supported: false,
|
||||||
|
authenticated: false,
|
||||||
|
authMethod: null,
|
||||||
|
verificationState: 'error',
|
||||||
|
statusMessage: 'OpenCode CLI is not installed.',
|
||||||
|
models: [],
|
||||||
|
canLoginFromUi: false,
|
||||||
|
capabilities: {
|
||||||
|
teamLaunch: false,
|
||||||
|
oneShot: false,
|
||||||
|
},
|
||||||
|
backend: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(React.createElement(CliStatusBanner));
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('Downloading 42%');
|
||||||
|
const progressButton = Array.from(host.querySelectorAll('button')).find(
|
||||||
|
(button) => button.textContent?.trim() === 'Downloading 42%'
|
||||||
|
);
|
||||||
|
expect(progressButton).not.toBeUndefined();
|
||||||
|
expect(progressButton).toHaveProperty('disabled', true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always shows a provider-level Free models badge on the OpenCode dashboard card', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
storeState.cliInstallerState = 'idle';
|
||||||
|
storeState.cliStatus = createInstalledCliStatus({
|
||||||
|
flavor: 'agent_teams_orchestrator',
|
||||||
|
displayName: 'Multimodel runtime',
|
||||||
|
supportsSelfUpdate: false,
|
||||||
|
showVersionDetails: false,
|
||||||
|
showBinaryPath: false,
|
||||||
|
authLoggedIn: true,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
providerId: 'opencode',
|
||||||
|
displayName: 'OpenCode (75+ LLM providers)',
|
||||||
|
supported: true,
|
||||||
|
authenticated: true,
|
||||||
|
authMethod: 'opencode_managed',
|
||||||
|
verificationState: 'verified',
|
||||||
|
statusMessage: 'Ready',
|
||||||
|
models: [],
|
||||||
|
canLoginFromUi: false,
|
||||||
|
capabilities: {
|
||||||
|
teamLaunch: true,
|
||||||
|
oneShot: false,
|
||||||
|
},
|
||||||
|
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||||
|
modelCatalog: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(React.createElement(CliStatusBanner));
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const providerFreeBadge = host.querySelector('[title*="Big Pickle"]');
|
||||||
|
expect(providerFreeBadge?.textContent).toBe('Free models');
|
||||||
|
expect(providerFreeBadge?.getAttribute('title')).toContain('OpenRouter');
|
||||||
|
expect(providerFreeBadge?.getAttribute('title')).toContain(
|
||||||
|
'not every OpenCode/OpenRouter model is free'
|
||||||
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
|
||||||
|
|
@ -348,6 +348,7 @@ describe('ExtensionStoreView provider loading placeholders', () => {
|
||||||
|
|
||||||
expect(storeState.bootstrapCliStatus).toHaveBeenCalledWith({ multimodelEnabled: true });
|
expect(storeState.bootstrapCliStatus).toHaveBeenCalledWith({ multimodelEnabled: true });
|
||||||
expect(storeState.fetchCliStatus).not.toHaveBeenCalled();
|
expect(storeState.fetchCliStatus).not.toHaveBeenCalled();
|
||||||
|
expect(storeState.fetchApiKeys).not.toHaveBeenCalled();
|
||||||
|
|
||||||
expect(host.textContent).toContain('Multimodel runtime capabilities');
|
expect(host.textContent).toContain('Multimodel runtime capabilities');
|
||||||
expect(host.textContent).toContain('Anthropic');
|
expect(host.textContent).toContain('Anthropic');
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import type { CliInstallationStatus } from '@shared/types';
|
||||||
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
||||||
|
|
||||||
interface StoreState {
|
interface StoreState {
|
||||||
apiKeys: Array<{
|
apiKeys: {
|
||||||
id: string;
|
id: string;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
@ -15,13 +15,14 @@ interface StoreState {
|
||||||
scope: 'user';
|
scope: 'user';
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}>;
|
}[];
|
||||||
apiKeysLoading: boolean;
|
apiKeysLoading: boolean;
|
||||||
apiKeysError: string | null;
|
apiKeysError: string | null;
|
||||||
apiKeyStorageStatus: {
|
apiKeyStorageStatus: {
|
||||||
encryptionMethod: 'os-keychain' | 'local-aes';
|
encryptionMethod: 'os-keychain' | 'local-aes';
|
||||||
backend: string;
|
backend: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
fetchApiKeys: ReturnType<typeof vi.fn>;
|
||||||
fetchApiKeyStorageStatus: ReturnType<typeof vi.fn>;
|
fetchApiKeyStorageStatus: ReturnType<typeof vi.fn>;
|
||||||
cliStatus: CliInstallationStatus | null;
|
cliStatus: CliInstallationStatus | null;
|
||||||
cliStatusLoading: boolean;
|
cliStatusLoading: boolean;
|
||||||
|
|
@ -187,6 +188,7 @@ describe('ApiKeysPanel', () => {
|
||||||
encryptionMethod: 'os-keychain',
|
encryptionMethod: 'os-keychain',
|
||||||
backend: 'Keychain Access',
|
backend: 'Keychain Access',
|
||||||
};
|
};
|
||||||
|
storeState.fetchApiKeys = vi.fn().mockResolvedValue(undefined);
|
||||||
storeState.fetchApiKeyStorageStatus = vi.fn().mockResolvedValue(undefined);
|
storeState.fetchApiKeyStorageStatus = vi.fn().mockResolvedValue(undefined);
|
||||||
storeState.cliStatus = createCliStatus();
|
storeState.cliStatus = createCliStatus();
|
||||||
storeState.cliStatusLoading = false;
|
storeState.cliStatusLoading = false;
|
||||||
|
|
@ -253,6 +255,8 @@ describe('ApiKeysPanel', () => {
|
||||||
expect(host.textContent).toContain('Connected');
|
expect(host.textContent).toContain('Connected');
|
||||||
expect(host.textContent).toContain('Current source: Detected from OPENAI_API_KEY.');
|
expect(host.textContent).toContain('Current source: Detected from OPENAI_API_KEY.');
|
||||||
expect(host.textContent).toContain('ChatGPT account ready');
|
expect(host.textContent).toContain('ChatGPT account ready');
|
||||||
|
expect(storeState.fetchApiKeys).toHaveBeenCalledTimes(1);
|
||||||
|
expect(storeState.fetchApiKeyStorageStatus).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,115 @@ describe('ProviderModelBadges', () => {
|
||||||
expect(host.textContent).toContain('Free');
|
expect(host.textContent).toContain('Free');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('collapses long model lists and expands them into a bounded scroll area', () => {
|
it('renders paid and free OpenCode models together without marking every model free', () => {
|
||||||
|
const host = render(
|
||||||
|
<ProviderModelBadges
|
||||||
|
providerId="opencode"
|
||||||
|
models={['opencode/big-pickle', 'openai/gpt-5.4']}
|
||||||
|
providerStatus={{
|
||||||
|
providerId: 'opencode',
|
||||||
|
authMethod: 'opencode_managed',
|
||||||
|
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||||
|
modelCatalog: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
providerId: 'opencode',
|
||||||
|
source: 'app-server',
|
||||||
|
status: 'ready',
|
||||||
|
fetchedAt: '2026-05-12T00:00:00.000Z',
|
||||||
|
staleAt: '2026-05-12T00:10:00.000Z',
|
||||||
|
defaultModelId: 'opencode/big-pickle',
|
||||||
|
defaultLaunchModel: 'opencode/big-pickle',
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: 'opencode/big-pickle',
|
||||||
|
launchModel: 'opencode/big-pickle',
|
||||||
|
displayName: 'opencode/big-pickle',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: [],
|
||||||
|
defaultReasoningEffort: null,
|
||||||
|
inputModalities: ['text'],
|
||||||
|
supportsPersonality: true,
|
||||||
|
isDefault: true,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'app-server',
|
||||||
|
badgeLabel: 'Free',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'openai/gpt-5.4',
|
||||||
|
launchModel: 'openai/gpt-5.4',
|
||||||
|
displayName: 'openai/gpt-5.4',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: [],
|
||||||
|
defaultReasoningEffort: null,
|
||||||
|
inputModalities: ['text'],
|
||||||
|
supportsPersonality: true,
|
||||||
|
isDefault: false,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'app-server',
|
||||||
|
badgeLabel: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
diagnostics: {
|
||||||
|
configReadState: 'ready',
|
||||||
|
appServerState: 'healthy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('big-pickle');
|
||||||
|
expect(host.textContent).toContain('GPT-5.4');
|
||||||
|
expect(host.textContent?.match(/Free/g)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate a catalog badge that matches the displayed model label', () => {
|
||||||
|
const host = render(
|
||||||
|
<ProviderModelBadges
|
||||||
|
providerId="anthropic"
|
||||||
|
models={['claude-opus-4-6']}
|
||||||
|
providerStatus={{
|
||||||
|
providerId: 'anthropic',
|
||||||
|
authMethod: 'oauth_token',
|
||||||
|
backend: { kind: 'anthropic', label: 'Anthropic' },
|
||||||
|
modelCatalog: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
providerId: 'anthropic',
|
||||||
|
source: 'anthropic-models-api',
|
||||||
|
status: 'ready',
|
||||||
|
fetchedAt: '2026-05-12T00:00:00.000Z',
|
||||||
|
staleAt: '2026-05-12T00:10:00.000Z',
|
||||||
|
defaultModelId: 'claude-opus-4-6',
|
||||||
|
defaultLaunchModel: 'claude-opus-4-6',
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: 'claude-opus-4-6',
|
||||||
|
launchModel: 'claude-opus-4-6',
|
||||||
|
displayName: 'Opus 4.6',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: [],
|
||||||
|
defaultReasoningEffort: null,
|
||||||
|
inputModalities: ['text'],
|
||||||
|
supportsPersonality: true,
|
||||||
|
isDefault: true,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'anthropic-models-api',
|
||||||
|
badgeLabel: 'Opus 4.6',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
diagnostics: {
|
||||||
|
configReadState: 'ready',
|
||||||
|
appServerState: 'healthy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(host.textContent?.match(/Opus 4\.6/g)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collapses long model lists and expands them inline without an internal scroll area', () => {
|
||||||
const models = Array.from(
|
const models = Array.from(
|
||||||
{ length: 18 },
|
{ length: 18 },
|
||||||
(_, index) => `model-${String(index + 1).padStart(2, '0')}`
|
(_, index) => `model-${String(index + 1).padStart(2, '0')}`
|
||||||
|
|
@ -134,8 +242,8 @@ describe('ProviderModelBadges', () => {
|
||||||
expect(host.textContent).toContain('model-18');
|
expect(host.textContent).toContain('model-18');
|
||||||
expect(host.textContent).toContain('Hide');
|
expect(host.textContent).toContain('Hide');
|
||||||
const list = host.firstElementChild?.firstElementChild as HTMLElement | null;
|
const list = host.firstElementChild?.firstElementChild as HTMLElement | null;
|
||||||
expect(list?.style.maxHeight).toBe('200px');
|
expect(list?.style.maxHeight).toBe('');
|
||||||
expect(list?.style.overflowY).toBe('auto');
|
expect(list?.style.overflowY).toBe('');
|
||||||
|
|
||||||
const hideButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
const hideButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
button.textContent?.includes('Hide')
|
button.textContent?.includes('Hide')
|
||||||
|
|
@ -149,4 +257,41 @@ describe('ProviderModelBadges', () => {
|
||||||
expect(host.textContent).not.toContain('model-16');
|
expect(host.textContent).not.toContain('model-16');
|
||||||
expect(host.textContent).toContain('+3 more');
|
expect(host.textContent).toContain('+3 more');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('limits collapsed model badges by rendered rows when requested', () => {
|
||||||
|
const originalOffsetTop = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetTop');
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'offsetTop', {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
const siblings = Array.from(this.parentElement?.children ?? []);
|
||||||
|
const index = Math.max(0, siblings.indexOf(this));
|
||||||
|
return Math.floor(index / 3) * 20;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = Array.from(
|
||||||
|
{ length: 18 },
|
||||||
|
(_, index) => `model-${String(index + 1).padStart(2, '0')}`
|
||||||
|
);
|
||||||
|
const host = render(
|
||||||
|
<ProviderModelBadges
|
||||||
|
providerId="codex"
|
||||||
|
models={models}
|
||||||
|
collapseAfter={15}
|
||||||
|
maxCollapsedRows={2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('model-05');
|
||||||
|
expect(host.textContent).not.toContain('model-06');
|
||||||
|
expect(host.textContent).toContain('+13 more');
|
||||||
|
} finally {
|
||||||
|
if (originalOffsetTop) {
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'offsetTop', originalOffsetTop);
|
||||||
|
} else {
|
||||||
|
delete (HTMLElement.prototype as { offsetTop?: number }).offsetTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,66 @@ describe('MemberList spawn-status memoization', () => {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not label an empty roster as solo when the team summary still expects teammates', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(MemberList, {
|
||||||
|
members: [],
|
||||||
|
expectedTeammateCount: 2,
|
||||||
|
isTeamAlive: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('Team members are loading');
|
||||||
|
expect(host.textContent).not.toContain('Solo team');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render a lead-only roster while expected teammates are still loading', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(MemberList, {
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
...member,
|
||||||
|
name: 'team-lead',
|
||||||
|
agentType: 'team-lead',
|
||||||
|
role: 'Team Lead',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expectedTeammateCount: 2,
|
||||||
|
isTeamAlive: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('Team members are loading');
|
||||||
|
expect(host.querySelector('[data-testid="member-team-lead"]')).toBeNull();
|
||||||
|
expect(host.textContent).not.toContain('Solo team');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('rerenders cards when only the hard failure reason changes', async () => {
|
it('rerenders cards when only the hard failure reason changes', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
|
|
|
||||||
|
|
@ -178,8 +178,8 @@ describe('GraphView pan interactions', () => {
|
||||||
kind: 'task',
|
kind: 'task',
|
||||||
label: 'Task 1',
|
label: 'Task 1',
|
||||||
state: 'idle',
|
state: 'idle',
|
||||||
x: 160,
|
x: 300,
|
||||||
y: 90,
|
y: 180,
|
||||||
domainRef: { kind: 'task', teamName: 'demo-team', taskId: 'task:1' },
|
domainRef: { kind: 'task', teamName: 'demo-team', taskId: 'task:1' },
|
||||||
};
|
};
|
||||||
const edge: GraphEdge = {
|
const edge: GraphEdge = {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type { GraphNode } from '@claude-teams/agent-graph';
|
||||||
|
|
||||||
function createMockContext() {
|
function createMockContext() {
|
||||||
const arcCalls: Array<{ x: number; y: number; radius: number }> = [];
|
const arcCalls: Array<{ x: number; y: number; radius: number }> = [];
|
||||||
|
const fillTextCalls: Array<{ text: string; x: number; y: number }> = [];
|
||||||
const gradient = { addColorStop: vi.fn() };
|
const gradient = { addColorStop: vi.fn() };
|
||||||
let fillStyle: string | CanvasGradient | CanvasPattern = '';
|
let fillStyle: string | CanvasGradient | CanvasPattern = '';
|
||||||
let globalAlpha = 1;
|
let globalAlpha = 1;
|
||||||
|
|
@ -34,7 +35,9 @@ function createMockContext() {
|
||||||
createRadialGradient: vi.fn(() => gradient),
|
createRadialGradient: vi.fn(() => gradient),
|
||||||
createLinearGradient: vi.fn(() => gradient),
|
createLinearGradient: vi.fn(() => gradient),
|
||||||
measureText: vi.fn((text: string) => ({ width: text.length * 4.5 })),
|
measureText: vi.fn((text: string) => ({ width: text.length * 4.5 })),
|
||||||
fillText: vi.fn(),
|
fillText: vi.fn((text: string, x: number, y: number) => {
|
||||||
|
fillTextCalls.push({ text: String(text), x, y });
|
||||||
|
}),
|
||||||
strokeText: vi.fn(),
|
strokeText: vi.fn(),
|
||||||
shadowColor: '',
|
shadowColor: '',
|
||||||
shadowBlur: 0,
|
shadowBlur: 0,
|
||||||
|
|
@ -59,7 +62,7 @@ function createMockContext() {
|
||||||
},
|
},
|
||||||
} as unknown as CanvasRenderingContext2D;
|
} as unknown as CanvasRenderingContext2D;
|
||||||
|
|
||||||
return { ctx, arcCalls };
|
return { ctx, arcCalls, fillTextCalls };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTaskNode(hasLiveTaskLogs: boolean): GraphNode {
|
function createTaskNode(hasLiveTaskLogs: boolean): GraphNode {
|
||||||
|
|
@ -90,4 +93,37 @@ describe('drawTasks', () => {
|
||||||
expect(active.arcCalls.length).toBeGreaterThanOrEqual(3);
|
expect(active.arcCalls.length).toBeGreaterThanOrEqual(3);
|
||||||
expect(inactive.arcCalls).toHaveLength(0);
|
expect(inactive.arcCalls).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('wraps long task subjects into two canvas lines', () => {
|
||||||
|
const { ctx, fillTextCalls } = createMockContext();
|
||||||
|
const node = {
|
||||||
|
...createTaskNode(false),
|
||||||
|
displayId: '0f505654',
|
||||||
|
sublabel:
|
||||||
|
'Review VitePress docs: Get missing developer page ready for publishing search analytics routing navigation validation checklist and docs',
|
||||||
|
};
|
||||||
|
|
||||||
|
drawTasks(ctx, [node], 1, null, null, null, 1);
|
||||||
|
|
||||||
|
const firstTitleLine = fillTextCalls.find((call) => call.y === -16);
|
||||||
|
const secondTitleLine = fillTextCalls.find((call) => call.y === 2);
|
||||||
|
const displayId = fillTextCalls.find((call) => call.text === '0f505654');
|
||||||
|
|
||||||
|
expect(firstTitleLine?.text).toContain('Review VitePress docs');
|
||||||
|
expect(secondTitleLine?.text).toContain('...');
|
||||||
|
expect(displayId?.y).toBe(23);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the compact single-line subject layout for short titles', () => {
|
||||||
|
const { ctx, fillTextCalls } = createMockContext();
|
||||||
|
drawTasks(ctx, [createTaskNode(false)], 1, null, null, null, 1);
|
||||||
|
|
||||||
|
const titleLine = fillTextCalls.find((call) => call.text === 'Live log task');
|
||||||
|
const secondTitleLine = fillTextCalls.find((call) => call.y === 2);
|
||||||
|
const displayId = fillTextCalls.find((call) => call.text === '#1');
|
||||||
|
|
||||||
|
expect(titleLine?.y).toBe(-12);
|
||||||
|
expect(secondTitleLine).toBeUndefined();
|
||||||
|
expect(displayId?.y).toBe(12);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -344,6 +344,70 @@ describe('cliInstallerSlice', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('OpenCode runtime installer actions', () => {
|
||||||
|
it('refreshes OpenCode provider status after a successful app-managed install', async () => {
|
||||||
|
const placeholder = createMultimodelProvider({
|
||||||
|
providerId: 'opencode',
|
||||||
|
displayName: 'OpenCode',
|
||||||
|
supported: false,
|
||||||
|
authenticated: false,
|
||||||
|
authMethod: null,
|
||||||
|
verificationState: 'error',
|
||||||
|
statusMessage: 'OpenCode CLI is not installed.',
|
||||||
|
models: [],
|
||||||
|
canLoginFromUi: false,
|
||||||
|
capabilities: {
|
||||||
|
teamLaunch: false,
|
||||||
|
oneShot: false,
|
||||||
|
extensions: createDefaultCliExtensionCapabilities(),
|
||||||
|
},
|
||||||
|
backend: null,
|
||||||
|
});
|
||||||
|
const refreshed = createMultimodelProvider({
|
||||||
|
providerId: 'opencode',
|
||||||
|
displayName: 'OpenCode',
|
||||||
|
supported: true,
|
||||||
|
authenticated: true,
|
||||||
|
authMethod: 'opencode_managed',
|
||||||
|
models: ['opencode/big-pickle'],
|
||||||
|
canLoginFromUi: false,
|
||||||
|
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||||
|
});
|
||||||
|
|
||||||
|
useStore.setState({
|
||||||
|
cliStatus: createMultimodelStatus([placeholder]),
|
||||||
|
});
|
||||||
|
vi.mocked(api.openCodeRuntime.install).mockResolvedValue({
|
||||||
|
installed: true,
|
||||||
|
binaryPath: '/Users/tester/App Support/runtimes/opencode/current/opencode',
|
||||||
|
version: '1.14.48',
|
||||||
|
source: 'app-managed',
|
||||||
|
state: 'ready',
|
||||||
|
});
|
||||||
|
vi.mocked(api.cliInstaller.getProviderStatus).mockResolvedValue(refreshed);
|
||||||
|
|
||||||
|
await useStore.getState().installOpenCodeRuntime();
|
||||||
|
|
||||||
|
expect(api.openCodeRuntime.invalidateStatus).toHaveBeenCalledTimes(1);
|
||||||
|
expect(api.cliInstaller.invalidateStatus).toHaveBeenCalledTimes(1);
|
||||||
|
expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledWith('opencode');
|
||||||
|
expect(useStore.getState().openCodeRuntimeStatus).toMatchObject({
|
||||||
|
installed: true,
|
||||||
|
source: 'app-managed',
|
||||||
|
state: 'ready',
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
useStore
|
||||||
|
.getState()
|
||||||
|
.cliStatus?.providers.find((provider) => provider.providerId === 'opencode')
|
||||||
|
).toMatchObject({
|
||||||
|
supported: true,
|
||||||
|
authenticated: true,
|
||||||
|
models: ['opencode/big-pickle'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('fetchCliStatus', () => {
|
describe('fetchCliStatus', () => {
|
||||||
it('updates cliStatus from API', async () => {
|
it('updates cliStatus from API', async () => {
|
||||||
const mockStatus: CliInstallationStatus = {
|
const mockStatus: CliInstallationStatus = {
|
||||||
|
|
|
||||||
|
|
@ -1796,6 +1796,90 @@ describe('teamSlice actions', () => {
|
||||||
expect(selectResolvedMembersForTeamName(store.getState(), 'my-team')).toHaveLength(0);
|
expect(selectResolvedMembersForTeamName(store.getState(), 'my-team')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves a cached roster when full launch failure metadata confirms the member names', async () => {
|
||||||
|
const store = createSliceStore();
|
||||||
|
const cachedSnapshot = createTeamSnapshot({
|
||||||
|
config: { name: 'Cached Team' },
|
||||||
|
members: [
|
||||||
|
{ name: 'alice', role: 'developer', currentTaskId: null },
|
||||||
|
{ name: 'bob', role: 'reviewer', currentTaskId: null },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const leadOnlySnapshot = createTeamSnapshot({
|
||||||
|
config: { name: 'Lead Only Team' },
|
||||||
|
members: [{ name: 'team-lead', agentType: 'team-lead', currentTaskId: null }],
|
||||||
|
});
|
||||||
|
|
||||||
|
store.setState({
|
||||||
|
teamDataCacheByName: {
|
||||||
|
'my-team': cachedSnapshot,
|
||||||
|
},
|
||||||
|
teamByName: {
|
||||||
|
'my-team': {
|
||||||
|
teamName: 'my-team',
|
||||||
|
displayName: 'My Team',
|
||||||
|
description: '',
|
||||||
|
memberCount: 0,
|
||||||
|
expectedMemberCount: 2,
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['alice', 'bob'],
|
||||||
|
taskCount: 0,
|
||||||
|
lastActivity: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
hoisted.getData.mockResolvedValueOnce(leadOnlySnapshot);
|
||||||
|
|
||||||
|
await store.getState().selectTeam('my-team');
|
||||||
|
|
||||||
|
expect(store.getState().selectedTeamData).toEqual(cachedSnapshot);
|
||||||
|
expect(
|
||||||
|
selectResolvedMembersForTeamName(store.getState(), 'my-team').map((member) => member.name)
|
||||||
|
).toEqual(['alice', 'bob']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not preserve a cached roster when launch failure metadata only names part of the team', async () => {
|
||||||
|
const store = createSliceStore();
|
||||||
|
const cachedSnapshot = createTeamSnapshot({
|
||||||
|
config: { name: 'Cached Team' },
|
||||||
|
members: [
|
||||||
|
{ name: 'alice', role: 'developer', currentTaskId: null },
|
||||||
|
{ name: 'bob', role: 'reviewer', currentTaskId: null },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const leadOnlySnapshot = createTeamSnapshot({
|
||||||
|
config: { name: 'Lead Only Team' },
|
||||||
|
members: [{ name: 'team-lead', agentType: 'team-lead', currentTaskId: null }],
|
||||||
|
});
|
||||||
|
|
||||||
|
store.setState({
|
||||||
|
teamDataCacheByName: {
|
||||||
|
'my-team': cachedSnapshot,
|
||||||
|
},
|
||||||
|
teamByName: {
|
||||||
|
'my-team': {
|
||||||
|
teamName: 'my-team',
|
||||||
|
displayName: 'My Team',
|
||||||
|
description: '',
|
||||||
|
memberCount: 0,
|
||||||
|
expectedMemberCount: 2,
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['bob'],
|
||||||
|
taskCount: 0,
|
||||||
|
lastActivity: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
hoisted.getData.mockResolvedValueOnce(leadOnlySnapshot);
|
||||||
|
|
||||||
|
await store.getState().selectTeam('my-team');
|
||||||
|
|
||||||
|
expect(store.getState().selectedTeamData).toEqual(leadOnlySnapshot);
|
||||||
|
expect(
|
||||||
|
selectResolvedMembersForTeamName(store.getState(), 'my-team').map((member) => member.name)
|
||||||
|
).toEqual(['team-lead']);
|
||||||
|
});
|
||||||
|
|
||||||
it('commits a late selectTeam snapshot that explicitly marks members as removed', async () => {
|
it('commits a late selectTeam snapshot that explicitly marks members as removed', async () => {
|
||||||
const store = createSliceStore();
|
const store = createSliceStore();
|
||||||
const selectRequest = createDeferredPromise<ReturnType<typeof createTeamSnapshot>>();
|
const selectRequest = createDeferredPromise<ReturnType<typeof createTeamSnapshot>>();
|
||||||
|
|
@ -3077,6 +3161,61 @@ describe('teamSlice actions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not synthesize member cards from launch failure names when summary roster is missing', () => {
|
||||||
|
const store = createSliceStore();
|
||||||
|
const leadOnlySnapshot = createTeamSnapshot({
|
||||||
|
config: {
|
||||||
|
name: 'My Team',
|
||||||
|
projectPath: '/repo',
|
||||||
|
},
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
name: 'team-lead',
|
||||||
|
agentType: 'team-lead',
|
||||||
|
currentTaskId: null,
|
||||||
|
role: 'Lead from detail',
|
||||||
|
color: 'purple',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
id: 'task-1',
|
||||||
|
subject: 'Build',
|
||||||
|
status: 'in_progress',
|
||||||
|
owner: 'Alice',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
store.setState({
|
||||||
|
selectedTeamName: 'my-team',
|
||||||
|
selectedTeamData: leadOnlySnapshot,
|
||||||
|
teamDataCacheByName: {
|
||||||
|
'my-team': leadOnlySnapshot,
|
||||||
|
},
|
||||||
|
teamByName: {
|
||||||
|
'my-team': {
|
||||||
|
teamName: 'my-team',
|
||||||
|
displayName: 'My Team',
|
||||||
|
description: '',
|
||||||
|
memberCount: 0,
|
||||||
|
expectedMemberCount: 2,
|
||||||
|
leadName: 'Lead',
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['Lead', 'Alice', 'bob'],
|
||||||
|
taskCount: 1,
|
||||||
|
lastActivity: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberActivityMetaByTeam: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const members = selectResolvedMembersForTeamName(store.getState(), 'my-team');
|
||||||
|
|
||||||
|
expect(members.map((m) => m.name)).toEqual(['team-lead']);
|
||||||
|
expect(selectResolvedMemberForTeamName(store.getState(), 'my-team', 'Alice')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('memoizes team-scoped member messages selectors over the merged message feed', () => {
|
it('memoizes team-scoped member messages selectors over the merged message feed', () => {
|
||||||
const store = createSliceStore();
|
const store = createSliceStore();
|
||||||
store.setState({
|
store.setState({
|
||||||
|
|
|
||||||
|
|
@ -162,24 +162,24 @@ describe('teamModelAvailability', () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(getAvailableTeamProviderModels('opencode', providerStatus)).toEqual([
|
expect(getAvailableTeamProviderModels('opencode', providerStatus)).toEqual([
|
||||||
'openai/gpt-5.4',
|
|
||||||
'opencode/big-pickle',
|
'opencode/big-pickle',
|
||||||
|
'openai/gpt-5.4',
|
||||||
'openrouter/moonshotai/kimi-k2',
|
'openrouter/moonshotai/kimi-k2',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(getAvailableTeamProviderModelOptions('opencode', providerStatus)).toEqual([
|
expect(getAvailableTeamProviderModelOptions('opencode', providerStatus)).toEqual([
|
||||||
{ value: '', label: 'Default', badgeLabel: 'Default' },
|
{ value: '', label: 'Default', badgeLabel: 'Default' },
|
||||||
{
|
{
|
||||||
value: 'openai/gpt-5.4',
|
value: 'opencode/big-pickle',
|
||||||
label: 'GPT-5.4',
|
label: 'big-pickle',
|
||||||
badgeLabel: 'OpenAI',
|
badgeLabel: 'OpenCode',
|
||||||
availabilityStatus: 'available',
|
availabilityStatus: 'available',
|
||||||
availabilityReason: null,
|
availabilityReason: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'opencode/big-pickle',
|
value: 'openai/gpt-5.4',
|
||||||
label: 'big-pickle',
|
label: 'GPT-5.4',
|
||||||
badgeLabel: 'OpenCode',
|
badgeLabel: 'OpenAI',
|
||||||
availabilityStatus: 'available',
|
availabilityStatus: 'available',
|
||||||
availabilityReason: null,
|
availabilityReason: null,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,102 @@ describe('teamModelCatalog', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('orders OpenCode free models before paid models', () => {
|
||||||
|
expect(
|
||||||
|
getVisibleTeamProviderModels(
|
||||||
|
'opencode',
|
||||||
|
[
|
||||||
|
'openrouter/deepseek/deepseek-r1',
|
||||||
|
'openai/gpt-5.4',
|
||||||
|
'openrouter/openai/gpt-oss-20b:free',
|
||||||
|
'opencode/big-pickle',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
providerId: 'opencode',
|
||||||
|
authMethod: 'opencode_managed',
|
||||||
|
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||||
|
modelCatalog: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
providerId: 'opencode',
|
||||||
|
source: 'app-server',
|
||||||
|
status: 'ready',
|
||||||
|
fetchedAt: '2026-05-12T00:00:00.000Z',
|
||||||
|
staleAt: '2026-05-12T00:10:00.000Z',
|
||||||
|
defaultModelId: 'opencode/big-pickle',
|
||||||
|
defaultLaunchModel: 'opencode/big-pickle',
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: 'opencode/big-pickle',
|
||||||
|
launchModel: 'opencode/big-pickle',
|
||||||
|
displayName: 'opencode/big-pickle',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: [],
|
||||||
|
defaultReasoningEffort: null,
|
||||||
|
inputModalities: ['text'],
|
||||||
|
supportsPersonality: true,
|
||||||
|
isDefault: true,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'app-server',
|
||||||
|
badgeLabel: 'Free',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'openrouter/openai/gpt-oss-20b:free',
|
||||||
|
launchModel: 'openrouter/openai/gpt-oss-20b:free',
|
||||||
|
displayName: 'openrouter/openai/gpt-oss-20b:free',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: [],
|
||||||
|
defaultReasoningEffort: null,
|
||||||
|
inputModalities: ['text'],
|
||||||
|
supportsPersonality: true,
|
||||||
|
isDefault: false,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'app-server',
|
||||||
|
badgeLabel: 'Free',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'openai/gpt-5.4',
|
||||||
|
launchModel: 'openai/gpt-5.4',
|
||||||
|
displayName: 'openai/gpt-5.4',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: [],
|
||||||
|
defaultReasoningEffort: null,
|
||||||
|
inputModalities: ['text'],
|
||||||
|
supportsPersonality: true,
|
||||||
|
isDefault: false,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'app-server',
|
||||||
|
badgeLabel: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'openrouter/deepseek/deepseek-r1',
|
||||||
|
launchModel: 'openrouter/deepseek/deepseek-r1',
|
||||||
|
displayName: 'openrouter/deepseek/deepseek-r1',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: [],
|
||||||
|
defaultReasoningEffort: null,
|
||||||
|
inputModalities: ['text'],
|
||||||
|
supportsPersonality: true,
|
||||||
|
isDefault: false,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'app-server',
|
||||||
|
badgeLabel: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
diagnostics: {
|
||||||
|
configReadState: 'ready',
|
||||||
|
appServerState: 'healthy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
'opencode/big-pickle',
|
||||||
|
'openrouter/openai/gpt-oss-20b:free',
|
||||||
|
'openai/gpt-5.4',
|
||||||
|
'openrouter/deepseek/deepseek-r1',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('detects Sonnet aliases with or without 1M suffix', () => {
|
it('detects Sonnet aliases with or without 1M suffix', () => {
|
||||||
expect(isAnthropicSonnetTeamModel('sonnet')).toBe(true);
|
expect(isAnthropicSonnetTeamModel('sonnet')).toBe(true);
|
||||||
expect(isAnthropicSonnetTeamModel('sonnet[1m]')).toBe(true);
|
expect(isAnthropicSonnetTeamModel('sonnet[1m]')).toBe(true);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue