Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -87,6 +87,7 @@ interface SplitContainerProps {
|
|||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onCopyFilePath: (path: string) => Promise<void> | void;
|
||||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||||
onCloseTabsToLeft: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
|
onCloseTabsToLeft: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
|
||||||
@@ -365,6 +366,7 @@ export function SplitContainer({
|
|||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
@@ -581,6 +583,7 @@ export function SplitContainer({
|
|||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
|
onCopyFilePath={onCopyFilePath}
|
||||||
onReloadAgent={onReloadAgent}
|
onReloadAgent={onReloadAgent}
|
||||||
onRenameTab={onRenameTab}
|
onRenameTab={onRenameTab}
|
||||||
onCloseTabsToLeft={onCloseTabsToLeft}
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
||||||
@@ -723,6 +726,7 @@ function SplitNodeView({
|
|||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
@@ -775,6 +779,7 @@ function SplitNodeView({
|
|||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
|
onCopyFilePath={onCopyFilePath}
|
||||||
onReloadAgent={onReloadAgent}
|
onReloadAgent={onReloadAgent}
|
||||||
onRenameTab={onRenameTab}
|
onRenameTab={onRenameTab}
|
||||||
onCloseTabsToLeft={onCloseTabsToLeft}
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
||||||
@@ -820,6 +825,7 @@ function SplitNodeView({
|
|||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
|
onCopyFilePath={onCopyFilePath}
|
||||||
onReloadAgent={onReloadAgent}
|
onReloadAgent={onReloadAgent}
|
||||||
onRenameTab={onRenameTab}
|
onRenameTab={onRenameTab}
|
||||||
onCloseTabsToLeft={onCloseTabsToLeft}
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
||||||
@@ -871,6 +877,7 @@ function SplitPaneView({
|
|||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
@@ -1012,6 +1019,7 @@ function SplitPaneView({
|
|||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
|
onCopyFilePath={onCopyFilePath}
|
||||||
onReloadAgent={onReloadAgent}
|
onReloadAgent={onReloadAgent}
|
||||||
onRenameTab={onRenameTab}
|
onRenameTab={onRenameTab}
|
||||||
onCloseTabsToLeft={handleCloseTabsToLeft}
|
onCloseTabsToLeft={handleCloseTabsToLeft}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const SNAP_POINTS: string[] = ["82%", "94%"];
|
|||||||
export function PairDeviceModal({ visible, onClose, testID }: PairDeviceModalProps) {
|
export function PairDeviceModal({ visible, onClose, testID }: PairDeviceModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const header = useMemo<SheetHeader>(
|
const header = useMemo<SheetHeader>(
|
||||||
() => ({ title: t("settings.integrations.pairDevices.rowTitle") }),
|
() => ({ title: t("settings.host.pairDevices.rowTitle") }),
|
||||||
[t],
|
[t],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -433,6 +433,7 @@ export const ar: TranslationResources = {
|
|||||||
openFor: "فتح القائمة لـ{{label}}",
|
openFor: "فتح القائمة لـ{{label}}",
|
||||||
copyResumeCommand: "نسخ أمر السيرة الذاتية",
|
copyResumeCommand: "نسخ أمر السيرة الذاتية",
|
||||||
copyAgentId: "نسخ معرف الوكيل",
|
copyAgentId: "نسخ معرف الوكيل",
|
||||||
|
copyFilePath: "Copy file path",
|
||||||
rename: "إعادة تسمية",
|
rename: "إعادة تسمية",
|
||||||
closeAbove: "إغلاق علامات التبويب أعلاه",
|
closeAbove: "إغلاق علامات التبويب أعلاه",
|
||||||
closeBelow: "إغلاق علامات التبويب أدناه",
|
closeBelow: "إغلاق علامات التبويب أدناه",
|
||||||
@@ -467,6 +468,7 @@ export const ar: TranslationResources = {
|
|||||||
copyFailed: "فشل النسخ",
|
copyFailed: "فشل النسخ",
|
||||||
agentIdCopiedLabel: "AgentID",
|
agentIdCopiedLabel: "AgentID",
|
||||||
resumeCommandCopiedLabel: "أمر الاستئناف",
|
resumeCommandCopiedLabel: "أمر الاستئناف",
|
||||||
|
filePathCopiedLabel: "File path",
|
||||||
resumeIdUnavailable: "السيرة الذاتية ID غير متوفرة",
|
resumeIdUnavailable: "السيرة الذاتية ID غير متوفرة",
|
||||||
resumeCommandUnavailable: "أمر الاستئناف غير متوفر",
|
resumeCommandUnavailable: "أمر الاستئناف غير متوفر",
|
||||||
reloadingAgent: "وكيل إعادة التحميل...",
|
reloadingAgent: "وكيل إعادة التحميل...",
|
||||||
@@ -1294,7 +1296,7 @@ export const ar: TranslationResources = {
|
|||||||
},
|
},
|
||||||
renameModal: {
|
renameModal: {
|
||||||
rename: "إعادة تسمية",
|
rename: "إعادة تسمية",
|
||||||
saving: "توفير...",
|
saving: "جارٍ الحفظ...",
|
||||||
},
|
},
|
||||||
sidebarCallout: {
|
sidebarCallout: {
|
||||||
dismiss: "رفض",
|
dismiss: "رفض",
|
||||||
@@ -1612,6 +1614,10 @@ export const ar: TranslationResources = {
|
|||||||
sheetTitle: "إلحاق موجه النظام",
|
sheetTitle: "إلحاق موجه النظام",
|
||||||
accessibilityLabel: "إلحاق موجه النظام",
|
accessibilityLabel: "إلحاق موجه النظام",
|
||||||
placeholder: "اجعل الردود موجزة دائمًا.",
|
placeholder: "اجعل الردود موجزة دائمًا.",
|
||||||
|
edit: "تحرير",
|
||||||
|
reset: "إعادة ضبط",
|
||||||
|
save: "حفظ",
|
||||||
|
saving: "جارٍ الحفظ...",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
@@ -1646,10 +1652,12 @@ export const ar: TranslationResources = {
|
|||||||
editLabel: "تحرير التسمية",
|
editLabel: "تحرير التسمية",
|
||||||
title: "إعادة تسمية المضيف",
|
title: "إعادة تسمية المضيف",
|
||||||
placeholder: "بلدي Host",
|
placeholder: "بلدي Host",
|
||||||
|
submit: "إعادة تسمية",
|
||||||
},
|
},
|
||||||
restart: {
|
restart: {
|
||||||
title: "إعادة تشغيل البرنامج الخفي",
|
title: "إعادة تشغيل البرنامج الخفي",
|
||||||
hint: "إعادة تشغيل عملية البرنامج الخفي. سيتم إعادة الاتصال بالتطبيق تلقائيًا",
|
hint: "إعادة تشغيل عملية البرنامج الخفي. سيتم إعادة الاتصال بالتطبيق تلقائيًا",
|
||||||
|
confirm: "إعادة التشغيل",
|
||||||
confirmTitle: "أعد تشغيل{{name}}",
|
confirmTitle: "أعد تشغيل{{name}}",
|
||||||
confirmMessage:
|
confirmMessage:
|
||||||
"سيؤدي هذا إلى إعادة تشغيل البرنامج الخفي. سيستمر العملاء الذين يعملون عليه؛ سيتم إعادة الاتصال بالتطبيق تلقائيًا.",
|
"سيؤدي هذا إلى إعادة تشغيل البرنامج الخفي. سيستمر العملاء الذين يعملون عليه؛ سيتم إعادة الاتصال بالتطبيق تلقائيًا.",
|
||||||
@@ -1815,9 +1823,9 @@ export const ar: TranslationResources = {
|
|||||||
actions: {
|
actions: {
|
||||||
reload: "إعادة تحميل",
|
reload: "إعادة تحميل",
|
||||||
tryAgain: "حاول ثانية",
|
tryAgain: "حاول ثانية",
|
||||||
save: "يحفظ",
|
save: "حفظ",
|
||||||
saved: "تم حفظ المشروع",
|
saved: "تم حفظ المشروع",
|
||||||
saving: "توفير...",
|
saving: "جارٍ الحفظ...",
|
||||||
cancel: "يلغي",
|
cancel: "يلغي",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ export const en = {
|
|||||||
openFor: "Open menu for {{label}}",
|
openFor: "Open menu for {{label}}",
|
||||||
copyResumeCommand: "Copy resume command",
|
copyResumeCommand: "Copy resume command",
|
||||||
copyAgentId: "Copy agent id",
|
copyAgentId: "Copy agent id",
|
||||||
|
copyFilePath: "Copy file path",
|
||||||
rename: "Rename",
|
rename: "Rename",
|
||||||
closeAbove: "Close tabs above",
|
closeAbove: "Close tabs above",
|
||||||
closeBelow: "Close tabs below",
|
closeBelow: "Close tabs below",
|
||||||
@@ -466,6 +467,7 @@ export const en = {
|
|||||||
copyFailed: "Copy failed",
|
copyFailed: "Copy failed",
|
||||||
agentIdCopiedLabel: "Agent ID",
|
agentIdCopiedLabel: "Agent ID",
|
||||||
resumeCommandCopiedLabel: "resume command",
|
resumeCommandCopiedLabel: "resume command",
|
||||||
|
filePathCopiedLabel: "File path",
|
||||||
resumeIdUnavailable: "Resume ID not available",
|
resumeIdUnavailable: "Resume ID not available",
|
||||||
resumeCommandUnavailable: "Resume command not available",
|
resumeCommandUnavailable: "Resume command not available",
|
||||||
reloadingAgent: "Reloading agent...",
|
reloadingAgent: "Reloading agent...",
|
||||||
@@ -1619,6 +1621,10 @@ export const en = {
|
|||||||
sheetTitle: "Append system prompt",
|
sheetTitle: "Append system prompt",
|
||||||
accessibilityLabel: "Append system prompt",
|
accessibilityLabel: "Append system prompt",
|
||||||
placeholder: "Always keep replies concise.",
|
placeholder: "Always keep replies concise.",
|
||||||
|
edit: "Edit",
|
||||||
|
reset: "Reset",
|
||||||
|
save: "Save",
|
||||||
|
saving: "Saving...",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
@@ -1653,10 +1659,12 @@ export const en = {
|
|||||||
editLabel: "Edit label",
|
editLabel: "Edit label",
|
||||||
title: "Rename host",
|
title: "Rename host",
|
||||||
placeholder: "My Host",
|
placeholder: "My Host",
|
||||||
|
submit: "Rename",
|
||||||
},
|
},
|
||||||
restart: {
|
restart: {
|
||||||
title: "Restart daemon",
|
title: "Restart daemon",
|
||||||
hint: "Restarts the daemon process. The app will reconnect automatically",
|
hint: "Restarts the daemon process. The app will reconnect automatically",
|
||||||
|
confirm: "Restart",
|
||||||
confirmTitle: "Restart {{name}}",
|
confirmTitle: "Restart {{name}}",
|
||||||
confirmMessage:
|
confirmMessage:
|
||||||
"This will restart the daemon. Agents running on it will keep going; the app will reconnect automatically.",
|
"This will restart the daemon. Agents running on it will keep going; the app will reconnect automatically.",
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ export const es: TranslationResources = {
|
|||||||
openFor: "Menú abierto para{{label}}",
|
openFor: "Menú abierto para{{label}}",
|
||||||
copyResumeCommand: "Copiar comando de reanudación",
|
copyResumeCommand: "Copiar comando de reanudación",
|
||||||
copyAgentId: "Copiar ID del agente",
|
copyAgentId: "Copiar ID del agente",
|
||||||
|
copyFilePath: "Copy file path",
|
||||||
rename: "Rebautizar",
|
rename: "Rebautizar",
|
||||||
closeAbove: "Cerrar pestañas arriba",
|
closeAbove: "Cerrar pestañas arriba",
|
||||||
closeBelow: "Cerrar pestañas a continuación",
|
closeBelow: "Cerrar pestañas a continuación",
|
||||||
@@ -471,6 +472,7 @@ export const es: TranslationResources = {
|
|||||||
copyFailed: "Copia fallida",
|
copyFailed: "Copia fallida",
|
||||||
agentIdCopiedLabel: "AgentID",
|
agentIdCopiedLabel: "AgentID",
|
||||||
resumeCommandCopiedLabel: "reanudar el comando",
|
resumeCommandCopiedLabel: "reanudar el comando",
|
||||||
|
filePathCopiedLabel: "File path",
|
||||||
resumeIdUnavailable: "ReanudarIDno disponible",
|
resumeIdUnavailable: "ReanudarIDno disponible",
|
||||||
resumeCommandUnavailable: "Comando de reanudación no disponible",
|
resumeCommandUnavailable: "Comando de reanudación no disponible",
|
||||||
reloadingAgent: "Agente de recarga...",
|
reloadingAgent: "Agente de recarga...",
|
||||||
@@ -1329,7 +1331,7 @@ export const es: TranslationResources = {
|
|||||||
},
|
},
|
||||||
renameModal: {
|
renameModal: {
|
||||||
rename: "Rebautizar",
|
rename: "Rebautizar",
|
||||||
saving: "Ahorro...",
|
saving: "Guardando...",
|
||||||
},
|
},
|
||||||
sidebarCallout: {
|
sidebarCallout: {
|
||||||
dismiss: "Despedir",
|
dismiss: "Despedir",
|
||||||
@@ -1649,6 +1651,10 @@ export const es: TranslationResources = {
|
|||||||
sheetTitle: "Agregar mensaje del sistema",
|
sheetTitle: "Agregar mensaje del sistema",
|
||||||
accessibilityLabel: "Agregar mensaje del sistema",
|
accessibilityLabel: "Agregar mensaje del sistema",
|
||||||
placeholder: "Mantenga siempre respuestas concisas.",
|
placeholder: "Mantenga siempre respuestas concisas.",
|
||||||
|
edit: "Editar",
|
||||||
|
reset: "Restablecer",
|
||||||
|
save: "Guardar",
|
||||||
|
saving: "Guardando...",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
@@ -1683,10 +1689,12 @@ export const es: TranslationResources = {
|
|||||||
editLabel: "Editar etiqueta",
|
editLabel: "Editar etiqueta",
|
||||||
title: "Cambiar nombre de host",
|
title: "Cambiar nombre de host",
|
||||||
placeholder: "MiHost",
|
placeholder: "MiHost",
|
||||||
|
submit: "Cambiar nombre",
|
||||||
},
|
},
|
||||||
restart: {
|
restart: {
|
||||||
title: "Reiniciar demonio",
|
title: "Reiniciar demonio",
|
||||||
hint: "Reinicia el proceso del demonio. La aplicación se volverá a conectar automáticamente",
|
hint: "Reinicia el proceso del demonio. La aplicación se volverá a conectar automáticamente",
|
||||||
|
confirm: "Reiniciar",
|
||||||
confirmTitle: "Reiniciar{{name}}",
|
confirmTitle: "Reiniciar{{name}}",
|
||||||
confirmMessage:
|
confirmMessage:
|
||||||
"Esto reiniciará el demonio. Los agentes que se ejecutan en él seguirán funcionando; la aplicación se volverá a conectar automáticamente.",
|
"Esto reiniciará el demonio. Los agentes que se ejecutan en él seguirán funcionando; la aplicación se volverá a conectar automáticamente.",
|
||||||
@@ -1856,9 +1864,9 @@ export const es: TranslationResources = {
|
|||||||
actions: {
|
actions: {
|
||||||
reload: "Recargar",
|
reload: "Recargar",
|
||||||
tryAgain: "Intentar otra vez",
|
tryAgain: "Intentar otra vez",
|
||||||
save: "Ahorrar",
|
save: "Guardar",
|
||||||
saved: "Proyecto guardado",
|
saved: "Proyecto guardado",
|
||||||
saving: "Ahorro...",
|
saving: "Guardando...",
|
||||||
cancel: "Cancelar",
|
cancel: "Cancelar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ export const fr: TranslationResources = {
|
|||||||
openFor: "Ouvrir le menu pour{{label}}",
|
openFor: "Ouvrir le menu pour{{label}}",
|
||||||
copyResumeCommand: "Copier la commande de reprise",
|
copyResumeCommand: "Copier la commande de reprise",
|
||||||
copyAgentId: "Copier l'identifiant de l'agent",
|
copyAgentId: "Copier l'identifiant de l'agent",
|
||||||
|
copyFilePath: "Copy file path",
|
||||||
rename: "Rebaptiser",
|
rename: "Rebaptiser",
|
||||||
closeAbove: "Fermer les onglets ci-dessus",
|
closeAbove: "Fermer les onglets ci-dessus",
|
||||||
closeBelow: "Fermer les onglets ci-dessous",
|
closeBelow: "Fermer les onglets ci-dessous",
|
||||||
@@ -471,6 +472,7 @@ export const fr: TranslationResources = {
|
|||||||
copyFailed: "Échec de la copie",
|
copyFailed: "Échec de la copie",
|
||||||
agentIdCopiedLabel: "AgentID",
|
agentIdCopiedLabel: "AgentID",
|
||||||
resumeCommandCopiedLabel: "reprendre la commande",
|
resumeCommandCopiedLabel: "reprendre la commande",
|
||||||
|
filePathCopiedLabel: "File path",
|
||||||
resumeIdUnavailable: "ReprendreIDnon disponible",
|
resumeIdUnavailable: "ReprendreIDnon disponible",
|
||||||
resumeCommandUnavailable: "Commande de reprise non disponible",
|
resumeCommandUnavailable: "Commande de reprise non disponible",
|
||||||
reloadingAgent: "Agent de rechargement...",
|
reloadingAgent: "Agent de rechargement...",
|
||||||
@@ -1332,7 +1334,7 @@ export const fr: TranslationResources = {
|
|||||||
},
|
},
|
||||||
renameModal: {
|
renameModal: {
|
||||||
rename: "Rebaptiser",
|
rename: "Rebaptiser",
|
||||||
saving: "Économie...",
|
saving: "Sauvegarde...",
|
||||||
},
|
},
|
||||||
sidebarCallout: {
|
sidebarCallout: {
|
||||||
dismiss: "Rejeter",
|
dismiss: "Rejeter",
|
||||||
@@ -1655,6 +1657,10 @@ export const fr: TranslationResources = {
|
|||||||
sheetTitle: "Ajouter une invite système",
|
sheetTitle: "Ajouter une invite système",
|
||||||
accessibilityLabel: "Ajouter une invite système",
|
accessibilityLabel: "Ajouter une invite système",
|
||||||
placeholder: "Gardez toujours des réponses concises.",
|
placeholder: "Gardez toujours des réponses concises.",
|
||||||
|
edit: "Modifier",
|
||||||
|
reset: "Réinitialiser",
|
||||||
|
save: "Sauvegarder",
|
||||||
|
saving: "Sauvegarde...",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
@@ -1689,10 +1695,12 @@ export const fr: TranslationResources = {
|
|||||||
editLabel: "Modifier l'étiquette",
|
editLabel: "Modifier l'étiquette",
|
||||||
title: "Renommer l'hôte",
|
title: "Renommer l'hôte",
|
||||||
placeholder: "MonHost",
|
placeholder: "MonHost",
|
||||||
|
submit: "Renommer",
|
||||||
},
|
},
|
||||||
restart: {
|
restart: {
|
||||||
title: "Redémarrer le démon",
|
title: "Redémarrer le démon",
|
||||||
hint: "Redémarre le processus démon. L'application se reconnectera automatiquement",
|
hint: "Redémarre le processus démon. L'application se reconnectera automatiquement",
|
||||||
|
confirm: "Redémarrer",
|
||||||
confirmTitle: "Redémarrer{{name}}",
|
confirmTitle: "Redémarrer{{name}}",
|
||||||
confirmMessage:
|
confirmMessage:
|
||||||
"Cela redémarrera le démon. Les agents qui s'y exécutent continueront à fonctionner; l'application se reconnectera automatiquement.",
|
"Cela redémarrera le démon. Les agents qui s'y exécutent continueront à fonctionner; l'application se reconnectera automatiquement.",
|
||||||
@@ -1866,7 +1874,7 @@ export const fr: TranslationResources = {
|
|||||||
tryAgain: "Essayer à nouveau",
|
tryAgain: "Essayer à nouveau",
|
||||||
save: "Sauvegarder",
|
save: "Sauvegarder",
|
||||||
saved: "Projet enregistré",
|
saved: "Projet enregistré",
|
||||||
saving: "Économie...",
|
saving: "Sauvegarde...",
|
||||||
cancel: "Annuler",
|
cancel: "Annuler",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ export const ru: TranslationResources = {
|
|||||||
openFor: "Открыть меню для{{label}}",
|
openFor: "Открыть меню для{{label}}",
|
||||||
copyResumeCommand: "Копировать команду возобновления",
|
copyResumeCommand: "Копировать команду возобновления",
|
||||||
copyAgentId: "Скопировать идентификатор агента",
|
copyAgentId: "Скопировать идентификатор агента",
|
||||||
|
copyFilePath: "Copy file path",
|
||||||
rename: "Переименовать",
|
rename: "Переименовать",
|
||||||
closeAbove: "Закрыть вкладки выше",
|
closeAbove: "Закрыть вкладки выше",
|
||||||
closeBelow: "Закройте вкладки ниже",
|
closeBelow: "Закройте вкладки ниже",
|
||||||
@@ -470,6 +471,7 @@ export const ru: TranslationResources = {
|
|||||||
copyFailed: "Не удалось скопировать",
|
copyFailed: "Не удалось скопировать",
|
||||||
agentIdCopiedLabel: "AgentID",
|
agentIdCopiedLabel: "AgentID",
|
||||||
resumeCommandCopiedLabel: "команда возобновления",
|
resumeCommandCopiedLabel: "команда возобновления",
|
||||||
|
filePathCopiedLabel: "File path",
|
||||||
resumeIdUnavailable: "Резюме ID недоступно",
|
resumeIdUnavailable: "Резюме ID недоступно",
|
||||||
resumeCommandUnavailable: "Команда возобновления недоступна",
|
resumeCommandUnavailable: "Команда возобновления недоступна",
|
||||||
reloadingAgent: "Перезагрузка агента...",
|
reloadingAgent: "Перезагрузка агента...",
|
||||||
@@ -1643,6 +1645,10 @@ export const ru: TranslationResources = {
|
|||||||
sheetTitle: "Добавить системное приглашение",
|
sheetTitle: "Добавить системное приглашение",
|
||||||
accessibilityLabel: "Добавить системное приглашение",
|
accessibilityLabel: "Добавить системное приглашение",
|
||||||
placeholder: "Всегда отвечайте кратко.",
|
placeholder: "Всегда отвечайте кратко.",
|
||||||
|
edit: "Редактировать",
|
||||||
|
reset: "Сбросить",
|
||||||
|
save: "Сохранить",
|
||||||
|
saving: "Сохранение...",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
@@ -1677,10 +1683,12 @@ export const ru: TranslationResources = {
|
|||||||
editLabel: "Изменить ярлык",
|
editLabel: "Изменить ярлык",
|
||||||
title: "Переименовать хост",
|
title: "Переименовать хост",
|
||||||
placeholder: "Мой Host",
|
placeholder: "Мой Host",
|
||||||
|
submit: "Переименовать",
|
||||||
},
|
},
|
||||||
restart: {
|
restart: {
|
||||||
title: "Перезапустить демон",
|
title: "Перезапустить демон",
|
||||||
hint: "Перезапускает процесс демона. Приложение автоматически переподключится",
|
hint: "Перезапускает процесс демона. Приложение автоматически переподключится",
|
||||||
|
confirm: "Перезапустить",
|
||||||
confirmTitle: "Перезапустите{{name}}",
|
confirmTitle: "Перезапустите{{name}}",
|
||||||
confirmMessage:
|
confirmMessage:
|
||||||
"Это перезапустит демон. Агенты, работающие на нем, будут продолжать работать; приложение автоматически повторно подключится.",
|
"Это перезапустит демон. Агенты, работающие на нем, будут продолжать работать; приложение автоматически повторно подключится.",
|
||||||
|
|||||||
@@ -433,6 +433,7 @@ export const zhCN: TranslationResources = {
|
|||||||
openFor: "打开 {{label}} 的菜单",
|
openFor: "打开 {{label}} 的菜单",
|
||||||
copyResumeCommand: "复制恢复命令",
|
copyResumeCommand: "复制恢复命令",
|
||||||
copyAgentId: "复制 Agent ID",
|
copyAgentId: "复制 Agent ID",
|
||||||
|
copyFilePath: "Copy file path",
|
||||||
rename: "重命名",
|
rename: "重命名",
|
||||||
closeAbove: "关闭上方标签",
|
closeAbove: "关闭上方标签",
|
||||||
closeBelow: "关闭下方标签",
|
closeBelow: "关闭下方标签",
|
||||||
@@ -467,6 +468,7 @@ export const zhCN: TranslationResources = {
|
|||||||
copyFailed: "复制失败",
|
copyFailed: "复制失败",
|
||||||
agentIdCopiedLabel: "Agent ID",
|
agentIdCopiedLabel: "Agent ID",
|
||||||
resumeCommandCopiedLabel: "恢复命令",
|
resumeCommandCopiedLabel: "恢复命令",
|
||||||
|
filePathCopiedLabel: "File path",
|
||||||
resumeIdUnavailable: "恢复 ID 不可用",
|
resumeIdUnavailable: "恢复 ID 不可用",
|
||||||
resumeCommandUnavailable: "恢复命令不可用",
|
resumeCommandUnavailable: "恢复命令不可用",
|
||||||
reloadingAgent: "正在重新加载 Agent...",
|
reloadingAgent: "正在重新加载 Agent...",
|
||||||
@@ -1594,6 +1596,10 @@ export const zhCN: TranslationResources = {
|
|||||||
sheetTitle: "追加 system prompt",
|
sheetTitle: "追加 system prompt",
|
||||||
accessibilityLabel: "追加 system prompt",
|
accessibilityLabel: "追加 system prompt",
|
||||||
placeholder: "始终保持回复简洁。",
|
placeholder: "始终保持回复简洁。",
|
||||||
|
edit: "编辑",
|
||||||
|
reset: "重置",
|
||||||
|
save: "保存",
|
||||||
|
saving: "保存中...",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
@@ -1628,10 +1634,12 @@ export const zhCN: TranslationResources = {
|
|||||||
editLabel: "编辑标签",
|
editLabel: "编辑标签",
|
||||||
title: "重命名 Host",
|
title: "重命名 Host",
|
||||||
placeholder: "我的 Host",
|
placeholder: "我的 Host",
|
||||||
|
submit: "重命名",
|
||||||
},
|
},
|
||||||
restart: {
|
restart: {
|
||||||
title: "重启 Daemon",
|
title: "重启 Daemon",
|
||||||
hint: "重启 Daemon 进程。应用会自动重新连接",
|
hint: "重启 Daemon 进程。应用会自动重新连接",
|
||||||
|
confirm: "重启",
|
||||||
confirmTitle: "重启 {{name}}",
|
confirmTitle: "重启 {{name}}",
|
||||||
confirmMessage: "这会重启 Daemon。其上运行的 Agent 会继续运行;应用会自动重新连接。",
|
confirmMessage: "这会重启 Daemon。其上运行的 Agent 会继续运行;应用会自动重新连接。",
|
||||||
restarting: "正在重启...",
|
restarting: "正在重启...",
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ interface WorkspaceDesktopTabsRowProps {
|
|||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onCopyFilePath: (path: string) => Promise<void> | void;
|
||||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||||
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
||||||
@@ -713,6 +714,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
@@ -784,6 +786,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
() => ({
|
() => ({
|
||||||
copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"),
|
copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"),
|
||||||
copyAgentId: t("workspace.tabs.menu.copyAgentId"),
|
copyAgentId: t("workspace.tabs.menu.copyAgentId"),
|
||||||
|
copyFilePath: t("workspace.tabs.menu.copyFilePath"),
|
||||||
rename: t("workspace.tabs.menu.rename"),
|
rename: t("workspace.tabs.menu.rename"),
|
||||||
closeAbove: t("workspace.tabs.menu.closeAbove"),
|
closeAbove: t("workspace.tabs.menu.closeAbove"),
|
||||||
closeBelow: t("workspace.tabs.menu.closeBelow"),
|
closeBelow: t("workspace.tabs.menu.closeBelow"),
|
||||||
@@ -881,6 +884,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
|
onCopyFilePath={onCopyFilePath}
|
||||||
onReloadAgent={onReloadAgent}
|
onReloadAgent={onReloadAgent}
|
||||||
onRenameTab={onRenameTab}
|
onRenameTab={onRenameTab}
|
||||||
onCloseTabsToLeft={onCloseTabsToLeft}
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
||||||
@@ -911,6 +915,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
onCloseTabsToRight,
|
onCloseTabsToRight,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onNavigateTab,
|
onNavigateTab,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
@@ -1003,6 +1008,7 @@ function ResolvedDesktopTabChip({
|
|||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
@@ -1028,6 +1034,7 @@ function ResolvedDesktopTabChip({
|
|||||||
normalizedWorkspaceId: string;
|
normalizedWorkspaceId: string;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onCopyFilePath: (path: string) => Promise<void> | void;
|
||||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||||
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
||||||
@@ -1053,6 +1060,7 @@ function ResolvedDesktopTabChip({
|
|||||||
tabCount,
|
tabCount,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -1069,6 +1077,7 @@ function ResolvedDesktopTabChip({
|
|||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
onCloseTabsToRight,
|
onCloseTabsToRight,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
labels,
|
labels,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ interface MobileWorkspaceTabSwitcherProps {
|
|||||||
onSelectSwitcherTab: (key: string) => void;
|
onSelectSwitcherTab: (key: string) => void;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onCopyFilePath: (path: string) => Promise<void> | void;
|
||||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
@@ -579,6 +580,7 @@ function MobileWorkspaceTabOption({
|
|||||||
onPress,
|
onPress,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -596,6 +598,7 @@ function MobileWorkspaceTabOption({
|
|||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onCopyFilePath: (path: string) => Promise<void> | void;
|
||||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
@@ -608,6 +611,7 @@ function MobileWorkspaceTabOption({
|
|||||||
() => ({
|
() => ({
|
||||||
copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"),
|
copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"),
|
||||||
copyAgentId: t("workspace.tabs.menu.copyAgentId"),
|
copyAgentId: t("workspace.tabs.menu.copyAgentId"),
|
||||||
|
copyFilePath: t("workspace.tabs.menu.copyFilePath"),
|
||||||
rename: t("workspace.tabs.menu.rename"),
|
rename: t("workspace.tabs.menu.rename"),
|
||||||
closeAbove: t("workspace.tabs.menu.closeAbove"),
|
closeAbove: t("workspace.tabs.menu.closeAbove"),
|
||||||
closeBelow: t("workspace.tabs.menu.closeBelow"),
|
closeBelow: t("workspace.tabs.menu.closeBelow"),
|
||||||
@@ -629,6 +633,7 @@ function MobileWorkspaceTabOption({
|
|||||||
menuTestIDBase,
|
menuTestIDBase,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -695,6 +700,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
|||||||
onSelectSwitcherTab,
|
onSelectSwitcherTab,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -750,6 +756,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
|||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
|
onCopyFilePath={onCopyFilePath}
|
||||||
onReloadAgent={onReloadAgent}
|
onReloadAgent={onReloadAgent}
|
||||||
onRenameTab={onRenameTab}
|
onRenameTab={onRenameTab}
|
||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
@@ -767,6 +774,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
|||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -2712,6 +2720,19 @@ function WorkspaceScreenContent({
|
|||||||
[toast, t],
|
[toast, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleCopyFilePath = useCallback(
|
||||||
|
async (path: string) => {
|
||||||
|
if (!path) return;
|
||||||
|
try {
|
||||||
|
await Clipboard.setStringAsync(path);
|
||||||
|
toast.copied(t("workspace.tabs.toasts.filePathCopiedLabel"));
|
||||||
|
} catch {
|
||||||
|
toast.error(t("workspace.tabs.toasts.copyFailed"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[toast, t],
|
||||||
|
);
|
||||||
|
|
||||||
const handleCopyResumeCommand = useCallback(
|
const handleCopyResumeCommand = useCallback(
|
||||||
async (agentId: string) => {
|
async (agentId: string) => {
|
||||||
if (!agentId) return;
|
if (!agentId) return;
|
||||||
@@ -3534,6 +3555,7 @@ function WorkspaceScreenContent({
|
|||||||
onCloseTab={handleCloseTabById}
|
onCloseTab={handleCloseTabById}
|
||||||
onCopyResumeCommand={handleCopyResumeCommand}
|
onCopyResumeCommand={handleCopyResumeCommand}
|
||||||
onCopyAgentId={handleCopyAgentId}
|
onCopyAgentId={handleCopyAgentId}
|
||||||
|
onCopyFilePath={handleCopyFilePath}
|
||||||
onReloadAgent={handleReloadAgent}
|
onReloadAgent={handleReloadAgent}
|
||||||
onRenameTab={handleRenameTab}
|
onRenameTab={handleRenameTab}
|
||||||
onCloseTabsToLeft={handleCloseTabsToLeftInPane}
|
onCloseTabsToLeft={handleCloseTabsToLeftInPane}
|
||||||
@@ -3568,6 +3590,7 @@ function WorkspaceScreenContent({
|
|||||||
handleCloseTabById,
|
handleCloseTabById,
|
||||||
handleCopyResumeCommand,
|
handleCopyResumeCommand,
|
||||||
handleCopyAgentId,
|
handleCopyAgentId,
|
||||||
|
handleCopyFilePath,
|
||||||
handleReloadAgent,
|
handleReloadAgent,
|
||||||
handleRenameTab,
|
handleRenameTab,
|
||||||
handleCloseTabsToLeftInPane,
|
handleCloseTabsToLeftInPane,
|
||||||
@@ -3648,6 +3671,7 @@ function WorkspaceScreenContent({
|
|||||||
onSelectSwitcherTab={handleSelectSwitcherTab}
|
onSelectSwitcherTab={handleSelectSwitcherTab}
|
||||||
onCopyResumeCommand={handleCopyResumeCommand}
|
onCopyResumeCommand={handleCopyResumeCommand}
|
||||||
onCopyAgentId={handleCopyAgentId}
|
onCopyAgentId={handleCopyAgentId}
|
||||||
|
onCopyFilePath={handleCopyFilePath}
|
||||||
onReloadAgent={handleReloadAgent}
|
onReloadAgent={handleReloadAgent}
|
||||||
onRenameTab={handleRenameTab}
|
onRenameTab={handleRenameTab}
|
||||||
onCloseTab={handleCloseTabById}
|
onCloseTab={handleCloseTabById}
|
||||||
@@ -3669,6 +3693,7 @@ function WorkspaceScreenContent({
|
|||||||
onCloseTab={handleCloseTabById}
|
onCloseTab={handleCloseTabById}
|
||||||
onCopyResumeCommand={handleCopyResumeCommand}
|
onCopyResumeCommand={handleCopyResumeCommand}
|
||||||
onCopyAgentId={handleCopyAgentId}
|
onCopyAgentId={handleCopyAgentId}
|
||||||
|
onCopyFilePath={handleCopyFilePath}
|
||||||
onReloadAgent={handleReloadAgent}
|
onReloadAgent={handleReloadAgent}
|
||||||
onRenameTab={handleRenameTab}
|
onRenameTab={handleRenameTab}
|
||||||
onCloseTabsToLeft={handleCloseTabsToLeft}
|
onCloseTabsToLeft={handleCloseTabsToLeft}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
it("uses desktop tab ordering labels for desktop menus", () => {
|
it("uses desktop tab ordering labels for desktop menus", () => {
|
||||||
const onCopyResumeCommand = vi.fn();
|
const onCopyResumeCommand = vi.fn();
|
||||||
const onCopyAgentId = vi.fn();
|
const onCopyAgentId = vi.fn();
|
||||||
|
const onCopyFilePath = vi.fn();
|
||||||
const onReloadAgent = vi.fn();
|
const onReloadAgent = vi.fn();
|
||||||
const onRenameTab = vi.fn();
|
const onRenameTab = vi.fn();
|
||||||
const onCloseTab = vi.fn();
|
const onCloseTab = vi.fn();
|
||||||
@@ -30,6 +31,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
menuTestIDBase: "workspace-tab-context-agent_123",
|
menuTestIDBase: "workspace-tab-context-agent_123",
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -59,6 +61,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
menuTestIDBase: "workspace-tab-menu-agent_123",
|
menuTestIDBase: "workspace-tab-menu-agent_123",
|
||||||
onCopyResumeCommand: vi.fn(),
|
onCopyResumeCommand: vi.fn(),
|
||||||
onCopyAgentId: vi.fn(),
|
onCopyAgentId: vi.fn(),
|
||||||
|
onCopyFilePath: vi.fn(),
|
||||||
onReloadAgent: vi.fn(),
|
onReloadAgent: vi.fn(),
|
||||||
onRenameTab: vi.fn(),
|
onRenameTab: vi.fn(),
|
||||||
onCloseTab: vi.fn(),
|
onCloseTab: vi.fn(),
|
||||||
@@ -93,6 +96,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
menuTestIDBase: "workspace-tab-menu-draft_123",
|
menuTestIDBase: "workspace-tab-menu-draft_123",
|
||||||
onCopyResumeCommand: vi.fn(),
|
onCopyResumeCommand: vi.fn(),
|
||||||
onCopyAgentId: vi.fn(),
|
onCopyAgentId: vi.fn(),
|
||||||
|
onCopyFilePath: vi.fn(),
|
||||||
onReloadAgent: vi.fn(),
|
onReloadAgent: vi.fn(),
|
||||||
onRenameTab: vi.fn(),
|
onRenameTab: vi.fn(),
|
||||||
onCloseTab: vi.fn(),
|
onCloseTab: vi.fn(),
|
||||||
@@ -120,6 +124,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
menuTestIDBase: "workspace-tab-context-agent_123",
|
menuTestIDBase: "workspace-tab-context-agent_123",
|
||||||
onCopyResumeCommand: vi.fn(),
|
onCopyResumeCommand: vi.fn(),
|
||||||
onCopyAgentId: vi.fn(),
|
onCopyAgentId: vi.fn(),
|
||||||
|
onCopyFilePath: vi.fn(),
|
||||||
onReloadAgent: vi.fn(),
|
onReloadAgent: vi.fn(),
|
||||||
onRenameTab: vi.fn(),
|
onRenameTab: vi.fn(),
|
||||||
onCloseTab: vi.fn(),
|
onCloseTab: vi.fn(),
|
||||||
@@ -148,6 +153,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
menuTestIDBase: "workspace-tab-context-agent_123",
|
menuTestIDBase: "workspace-tab-context-agent_123",
|
||||||
onCopyResumeCommand: vi.fn(),
|
onCopyResumeCommand: vi.fn(),
|
||||||
onCopyAgentId: vi.fn(),
|
onCopyAgentId: vi.fn(),
|
||||||
|
onCopyFilePath: vi.fn(),
|
||||||
onReloadAgent: vi.fn(),
|
onReloadAgent: vi.fn(),
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTab: vi.fn(),
|
onCloseTab: vi.fn(),
|
||||||
@@ -181,6 +187,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
menuTestIDBase: "workspace-tab-context-terminal_abc",
|
menuTestIDBase: "workspace-tab-context-terminal_abc",
|
||||||
onCopyResumeCommand: vi.fn(),
|
onCopyResumeCommand: vi.fn(),
|
||||||
onCopyAgentId: vi.fn(),
|
onCopyAgentId: vi.fn(),
|
||||||
|
onCopyFilePath: vi.fn(),
|
||||||
onReloadAgent: vi.fn(),
|
onReloadAgent: vi.fn(),
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTab: vi.fn(),
|
onCloseTab: vi.fn(),
|
||||||
@@ -193,6 +200,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
expect(labels[0]).toBe("Rename");
|
expect(labels[0]).toBe("Rename");
|
||||||
expect(labels).not.toContain("Copy resume command");
|
expect(labels).not.toContain("Copy resume command");
|
||||||
expect(labels).not.toContain("Copy agent id");
|
expect(labels).not.toContain("Copy agent id");
|
||||||
|
expect(labels).not.toContain("Copy file path");
|
||||||
expect(labels).not.toContain("Reload agent");
|
expect(labels).not.toContain("Reload agent");
|
||||||
|
|
||||||
const renameEntry = entries.find((entry) => entry.kind === "item" && entry.label === "Rename");
|
const renameEntry = entries.find((entry) => entry.kind === "item" && entry.label === "Rename");
|
||||||
@@ -203,6 +211,48 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
expect(onRenameTab).toHaveBeenCalledWith(terminalTab);
|
expect(onRenameTab).toHaveBeenCalledWith(terminalTab);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes copy file path for file tabs", () => {
|
||||||
|
const onCopyFilePath = vi.fn();
|
||||||
|
const fileTab: WorkspaceTabDescriptor = {
|
||||||
|
key: "file_abc",
|
||||||
|
tabId: "file_abc",
|
||||||
|
kind: "file",
|
||||||
|
target: { kind: "file", path: "/some/path.ts", lineStart: 1, lineEnd: 10 },
|
||||||
|
};
|
||||||
|
const entries = buildWorkspaceTabMenuEntries({
|
||||||
|
surface: "desktop",
|
||||||
|
tab: fileTab,
|
||||||
|
index: 0,
|
||||||
|
tabCount: 1,
|
||||||
|
menuTestIDBase: "workspace-tab-context-file_abc",
|
||||||
|
onCopyResumeCommand: vi.fn(),
|
||||||
|
onCopyAgentId: vi.fn(),
|
||||||
|
onCopyFilePath,
|
||||||
|
onReloadAgent: vi.fn(),
|
||||||
|
onRenameTab: vi.fn(),
|
||||||
|
onCloseTab: vi.fn(),
|
||||||
|
onCloseTabsBefore: vi.fn(),
|
||||||
|
onCloseTabsAfter: vi.fn(),
|
||||||
|
onCloseOtherTabs: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = entries.filter((entry) => entry.kind === "item").map((entry) => entry.label);
|
||||||
|
expect(labels[0]).toBe("Copy file path");
|
||||||
|
expect(labels).not.toContain("Copy resume command");
|
||||||
|
expect(labels).not.toContain("Copy agent id");
|
||||||
|
expect(labels).not.toContain("Rename");
|
||||||
|
expect(labels).not.toContain("Reload agent");
|
||||||
|
|
||||||
|
const copyFilePathEntry = entries.find(
|
||||||
|
(entry) => entry.kind === "item" && entry.key === "copy-file-path",
|
||||||
|
);
|
||||||
|
if (!copyFilePathEntry || copyFilePathEntry.kind !== "item") {
|
||||||
|
throw new Error("Copy file path entry missing");
|
||||||
|
}
|
||||||
|
copyFilePathEntry.onSelect();
|
||||||
|
expect(onCopyFilePath).toHaveBeenCalledWith("/some/path.ts");
|
||||||
|
});
|
||||||
|
|
||||||
it("uses the same rename entry shape for agent and terminal tabs", () => {
|
it("uses the same rename entry shape for agent and terminal tabs", () => {
|
||||||
const terminalTab: WorkspaceTabDescriptor = {
|
const terminalTab: WorkspaceTabDescriptor = {
|
||||||
key: "terminal_abc",
|
key: "terminal_abc",
|
||||||
@@ -218,6 +268,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
|||||||
menuTestIDBase,
|
menuTestIDBase,
|
||||||
onCopyResumeCommand: vi.fn(),
|
onCopyResumeCommand: vi.fn(),
|
||||||
onCopyAgentId: vi.fn(),
|
onCopyAgentId: vi.fn(),
|
||||||
|
onCopyFilePath: vi.fn(),
|
||||||
onReloadAgent: vi.fn(),
|
onReloadAgent: vi.fn(),
|
||||||
onRenameTab: vi.fn(),
|
onRenameTab: vi.fn(),
|
||||||
onCloseTab: vi.fn(),
|
onCloseTab: vi.fn(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type WorkspaceTabMenuSurface = "desktop" | "mobile";
|
|||||||
export interface WorkspaceTabMenuLabels {
|
export interface WorkspaceTabMenuLabels {
|
||||||
copyResumeCommand: string;
|
copyResumeCommand: string;
|
||||||
copyAgentId: string;
|
copyAgentId: string;
|
||||||
|
copyFilePath: string;
|
||||||
rename: string;
|
rename: string;
|
||||||
closeAbove: string;
|
closeAbove: string;
|
||||||
closeBelow: string;
|
closeBelow: string;
|
||||||
@@ -22,6 +23,7 @@ export interface WorkspaceTabMenuLabels {
|
|||||||
export const DEFAULT_WORKSPACE_TAB_MENU_LABELS: WorkspaceTabMenuLabels = {
|
export const DEFAULT_WORKSPACE_TAB_MENU_LABELS: WorkspaceTabMenuLabels = {
|
||||||
copyResumeCommand: i18n.t("workspace.tabs.menu.copyResumeCommand"),
|
copyResumeCommand: i18n.t("workspace.tabs.menu.copyResumeCommand"),
|
||||||
copyAgentId: i18n.t("workspace.tabs.menu.copyAgentId"),
|
copyAgentId: i18n.t("workspace.tabs.menu.copyAgentId"),
|
||||||
|
copyFilePath: i18n.t("workspace.tabs.menu.copyFilePath"),
|
||||||
rename: i18n.t("workspace.tabs.menu.rename"),
|
rename: i18n.t("workspace.tabs.menu.rename"),
|
||||||
closeAbove: i18n.t("workspace.tabs.menu.closeAbove"),
|
closeAbove: i18n.t("workspace.tabs.menu.closeAbove"),
|
||||||
closeBelow: i18n.t("workspace.tabs.menu.closeBelow"),
|
closeBelow: i18n.t("workspace.tabs.menu.closeBelow"),
|
||||||
@@ -66,6 +68,7 @@ interface BuildWorkspaceTabMenuEntriesInput {
|
|||||||
menuTestIDBase: string;
|
menuTestIDBase: string;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onCopyFilePath: (path: string) => Promise<void> | void;
|
||||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
@@ -81,6 +84,7 @@ interface BuildWorkspaceDesktopTabActionsInput {
|
|||||||
tabCount: number;
|
tabCount: number;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onCopyFilePath: (path: string) => Promise<void> | void;
|
||||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
@@ -148,6 +152,7 @@ export function buildWorkspaceTabMenuEntries(
|
|||||||
menuTestIDBase,
|
menuTestIDBase,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onCopyFilePath,
|
||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -186,6 +191,20 @@ export function buildWorkspaceTabMenuEntries(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab.target.kind === "file") {
|
||||||
|
const filePath = tab.target.path;
|
||||||
|
entries.push({
|
||||||
|
kind: "item",
|
||||||
|
key: "copy-file-path",
|
||||||
|
label: labels.copyFilePath,
|
||||||
|
icon: "copy",
|
||||||
|
testID: `${menuTestIDBase}-copy-file-path`,
|
||||||
|
onSelect: () => {
|
||||||
|
void onCopyFilePath(filePath);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (tab.target.kind === "agent" || tab.target.kind === "terminal") {
|
if (tab.target.kind === "agent" || tab.target.kind === "terminal") {
|
||||||
entries.push({
|
entries.push({
|
||||||
kind: "item",
|
kind: "item",
|
||||||
@@ -278,6 +297,7 @@ export function buildWorkspaceDesktopTabActions(
|
|||||||
menuTestIDBase: contextMenuTestId,
|
menuTestIDBase: contextMenuTestId,
|
||||||
onCopyResumeCommand: input.onCopyResumeCommand,
|
onCopyResumeCommand: input.onCopyResumeCommand,
|
||||||
onCopyAgentId: input.onCopyAgentId,
|
onCopyAgentId: input.onCopyAgentId,
|
||||||
|
onCopyFilePath: input.onCopyFilePath,
|
||||||
onReloadAgent: input.onReloadAgent,
|
onReloadAgent: input.onReloadAgent,
|
||||||
onRenameTab: input.onRenameTab,
|
onRenameTab: input.onRenameTab,
|
||||||
onCloseTab: input.onCloseTab,
|
onCloseTab: input.onCloseTab,
|
||||||
|
|||||||
@@ -992,6 +992,47 @@ test("createAgent injects paseo MCP server only into provider launch config", as
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("createAgent injects the MCP auth token as a bearer header into the launch config", async () => {
|
||||||
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
|
const storagePath = join(workdir, "agents");
|
||||||
|
const storage = new AgentStorage(storagePath, logger);
|
||||||
|
|
||||||
|
class CaptureClient extends TestAgentClient {
|
||||||
|
lastConfig: AgentSessionConfig | null = null;
|
||||||
|
|
||||||
|
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||||
|
this.lastConfig = config;
|
||||||
|
return new TestAgentSession(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new CaptureClient();
|
||||||
|
const manager = new AgentManager({
|
||||||
|
clients: {
|
||||||
|
codex: client,
|
||||||
|
},
|
||||||
|
registry: storage,
|
||||||
|
logger,
|
||||||
|
mcpBaseUrl: "http://127.0.0.1:6767/mcp/agents",
|
||||||
|
mcpAuthToken: "cap-token",
|
||||||
|
idFactory: () => "00000000-0000-4000-8000-000000000104",
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = await manager.createAgent({
|
||||||
|
provider: "codex",
|
||||||
|
cwd: workdir,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(manager.getMcpAuthToken()).toBe("cap-token");
|
||||||
|
expect(client.lastConfig?.mcpServers?.paseo).toEqual({
|
||||||
|
type: "http",
|
||||||
|
url: `http://127.0.0.1:6767/mcp/agents?callerAgentId=${snapshot.id}`,
|
||||||
|
headers: { Authorization: "Bearer cap-token" },
|
||||||
|
});
|
||||||
|
|
||||||
|
rmSync(workdir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
test("resumeAgentFromPersistence replaces stored internal paseo MCP with current runtime URL", async () => {
|
test("resumeAgentFromPersistence replaces stored internal paseo MCP with current runtime URL", async () => {
|
||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ export interface AgentManagerOptions {
|
|||||||
durableTimelineStore?: AgentTimelineStore;
|
durableTimelineStore?: AgentTimelineStore;
|
||||||
terminalManager?: TerminalManager | null;
|
terminalManager?: TerminalManager | null;
|
||||||
mcpBaseUrl?: string;
|
mcpBaseUrl?: string;
|
||||||
|
mcpAuthToken?: string;
|
||||||
appendSystemPrompt?: string;
|
appendSystemPrompt?: string;
|
||||||
agentStreamCoalesceWindowMs?: number;
|
agentStreamCoalesceWindowMs?: number;
|
||||||
rescueTimeouts?: AgentManagerRescueTimeouts;
|
rescueTimeouts?: AgentManagerRescueTimeouts;
|
||||||
@@ -482,6 +483,7 @@ export class AgentManager {
|
|||||||
private readonly backgroundTasks = new Set<Promise<void>>();
|
private readonly backgroundTasks = new Set<Promise<void>>();
|
||||||
private readonly agentStreamCoalescer: AgentStreamCoalescer;
|
private readonly agentStreamCoalescer: AgentStreamCoalescer;
|
||||||
private mcpBaseUrl: string | null;
|
private mcpBaseUrl: string | null;
|
||||||
|
private readonly mcpAuthToken: string | null;
|
||||||
private appendSystemPrompt: string;
|
private appendSystemPrompt: string;
|
||||||
private onAgentAttention?: AgentAttentionCallback;
|
private onAgentAttention?: AgentAttentionCallback;
|
||||||
private onAgentArchived?: AgentArchivedCallback;
|
private onAgentArchived?: AgentArchivedCallback;
|
||||||
@@ -496,6 +498,7 @@ export class AgentManager {
|
|||||||
this.onAgentAttention = options?.onAgentAttention;
|
this.onAgentAttention = options?.onAgentAttention;
|
||||||
this.onWorkspaceStateMayHaveChanged = options?.onWorkspaceStateMayHaveChanged;
|
this.onWorkspaceStateMayHaveChanged = options?.onWorkspaceStateMayHaveChanged;
|
||||||
this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
|
this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
|
||||||
|
this.mcpAuthToken = options?.mcpAuthToken ?? null;
|
||||||
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
|
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
|
||||||
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
||||||
this.rescueTimeouts = {
|
this.rescueTimeouts = {
|
||||||
@@ -554,6 +557,16 @@ export class AgentManager {
|
|||||||
this.mcpBaseUrl = url;
|
this.mcpBaseUrl = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capability token the daemon's own MCP clients must present to the Agent MCP
|
||||||
|
* endpoint when a daemon password is configured. Read by the per-client
|
||||||
|
* session to authenticate its own MCP connection. Stays in the daemon — never
|
||||||
|
* sent to remote clients.
|
||||||
|
*/
|
||||||
|
getMcpAuthToken(): string | null {
|
||||||
|
return this.mcpAuthToken;
|
||||||
|
}
|
||||||
|
|
||||||
setAppendSystemPrompt(prompt: string | null | undefined): void {
|
setAppendSystemPrompt(prompt: string | null | undefined): void {
|
||||||
this.appendSystemPrompt = prompt ?? "";
|
this.appendSystemPrompt = prompt ?? "";
|
||||||
}
|
}
|
||||||
@@ -3567,6 +3580,7 @@ export class AgentManager {
|
|||||||
config: storedConfig,
|
config: storedConfig,
|
||||||
agentId,
|
agentId,
|
||||||
mcpBaseUrl: this.mcpBaseUrl,
|
mcpBaseUrl: this.mcpBaseUrl,
|
||||||
|
mcpAuthToken: this.mcpAuthToken,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return { storedConfig, launchConfig };
|
return { storedConfig, launchConfig };
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
|
|
||||||
import { withTimeout } from "../../utils/promise-timeout.js";
|
import { withTimeout } from "../../utils/promise-timeout.js";
|
||||||
|
import { hashDaemonPassword } from "../auth.js";
|
||||||
import { createPaseoDaemon, type PaseoDaemonConfig } from "../bootstrap.js";
|
import { createPaseoDaemon, type PaseoDaemonConfig } from "../bootstrap.js";
|
||||||
import { createTestAgentClients } from "../test-utils/fake-agent-client.js";
|
import { createTestAgentClients } from "../test-utils/fake-agent-client.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -78,8 +79,11 @@ function getStructuredContent(result: McpToolResult): StructuredContent | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createMcpClient(url: string): Promise<McpClient> {
|
async function createMcpClient(url: string, authToken?: string): Promise<McpClient> {
|
||||||
const transport = new StreamableHTTPClientTransport(new URL(url));
|
const transport = new StreamableHTTPClientTransport(
|
||||||
|
new URL(url),
|
||||||
|
authToken ? { requestInit: { headers: { Authorization: `Bearer ${authToken}` } } } : undefined,
|
||||||
|
);
|
||||||
const rawClient = await experimental_createMCPClient({ transport });
|
const rawClient = await experimental_createMCPClient({ transport });
|
||||||
const boundCallTool: McpClient["callTool"] = Reflect.get(rawClient, "callTool").bind(rawClient);
|
const boundCallTool: McpClient["callTool"] = Reflect.get(rawClient, "callTool").bind(rawClient);
|
||||||
return { callTool: boundCallTool, close: () => rawClient.close() };
|
return { callTool: boundCallTool, close: () => rawClient.close() };
|
||||||
@@ -177,6 +181,76 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
}
|
}
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
|
test("password-protected daemon authorizes the agent MCP via the capability token", async () => {
|
||||||
|
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-"));
|
||||||
|
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
||||||
|
const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-"));
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
|
||||||
|
const daemonConfig: PaseoDaemonConfig = {
|
||||||
|
listen: `127.0.0.1:${port}`,
|
||||||
|
paseoHome,
|
||||||
|
corsAllowedOrigins: [],
|
||||||
|
hostnames: true,
|
||||||
|
mcpEnabled: true,
|
||||||
|
staticDir,
|
||||||
|
mcpDebug: false,
|
||||||
|
agentClients: createTestAgentClients(),
|
||||||
|
agentStoragePath: path.join(paseoHome, "agents"),
|
||||||
|
auth: { password: hashDaemonPassword("daemon-secret") },
|
||||||
|
};
|
||||||
|
|
||||||
|
const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" }));
|
||||||
|
await daemon.start();
|
||||||
|
|
||||||
|
const mcpUrl = `http://127.0.0.1:${port}/mcp/agents`;
|
||||||
|
const capabilityToken = daemon.agentManager.getMcpAuthToken();
|
||||||
|
expect(typeof capabilityToken).toBe("string");
|
||||||
|
|
||||||
|
let agentId: string | null = null;
|
||||||
|
let client: McpClient | null = null;
|
||||||
|
try {
|
||||||
|
// Remote auth is not weakened: a request without credentials is rejected
|
||||||
|
// before any MCP processing.
|
||||||
|
const unauthorized = await fetch(mcpUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: "{}",
|
||||||
|
});
|
||||||
|
expect(unauthorized.status).toBe(401);
|
||||||
|
|
||||||
|
// The injected capability token authenticates the full MCP handshake:
|
||||||
|
// creating (and connecting) the client and driving a tool call both go
|
||||||
|
// through the password-gated /mcp/agents route. (The exact bearer header
|
||||||
|
// injected into a child agent's config is covered by the
|
||||||
|
// runtime-mcp-config unit test.)
|
||||||
|
client = await createMcpClient(mcpUrl, capabilityToken!);
|
||||||
|
const result = await client.callTool({
|
||||||
|
name: "create_agent",
|
||||||
|
args: {
|
||||||
|
cwd: agentCwd,
|
||||||
|
title: "Password MCP",
|
||||||
|
provider: "claude/claude-test-model",
|
||||||
|
mode: "bypassPermissions",
|
||||||
|
initialPrompt: "reply with done and stop",
|
||||||
|
background: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const payload = getStructuredContent(result);
|
||||||
|
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
||||||
|
expect(agentId).toBeTruthy();
|
||||||
|
} finally {
|
||||||
|
if (agentId) {
|
||||||
|
await client?.callTool({ name: "kill_agent", args: { agentId } });
|
||||||
|
}
|
||||||
|
await client?.close();
|
||||||
|
await daemon.stop();
|
||||||
|
await rm(paseoHome, { recursive: true, force: true });
|
||||||
|
await rm(staticDir, { recursive: true, force: true });
|
||||||
|
await rm(agentCwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
test("create_agent auto-injects paseo MCP by default and can be disabled", async () => {
|
test("create_agent auto-injects paseo MCP by default and can be disabled", async () => {
|
||||||
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-"));
|
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-"));
|
||||||
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import type { AgentSessionConfig } from "./agent-sdk-types.js";
|
||||||
|
import { withRuntimePaseoMcpServer } from "./runtime-mcp-config.js";
|
||||||
|
|
||||||
|
const BASE_CONFIG: AgentSessionConfig = {
|
||||||
|
provider: "claude",
|
||||||
|
cwd: "/tmp/agent",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("withRuntimePaseoMcpServer", () => {
|
||||||
|
test("injects the paseo MCP server with a bearer header when a token is provided", () => {
|
||||||
|
const result = withRuntimePaseoMcpServer({
|
||||||
|
config: BASE_CONFIG,
|
||||||
|
agentId: "agent-1",
|
||||||
|
mcpBaseUrl: "http://127.0.0.1:6767/mcp/agents",
|
||||||
|
mcpAuthToken: "cap-token",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.mcpServers?.paseo).toEqual({
|
||||||
|
type: "http",
|
||||||
|
url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=agent-1",
|
||||||
|
headers: { Authorization: "Bearer cap-token" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("omits the header when no token is available", () => {
|
||||||
|
const result = withRuntimePaseoMcpServer({
|
||||||
|
config: BASE_CONFIG,
|
||||||
|
agentId: "agent-1",
|
||||||
|
mcpBaseUrl: "http://127.0.0.1:6767/mcp/agents",
|
||||||
|
mcpAuthToken: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.mcpServers?.paseo).toEqual({
|
||||||
|
type: "http",
|
||||||
|
url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=agent-1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not inject when no MCP base URL is configured", () => {
|
||||||
|
const result = withRuntimePaseoMcpServer({
|
||||||
|
config: BASE_CONFIG,
|
||||||
|
agentId: "agent-1",
|
||||||
|
mcpBaseUrl: null,
|
||||||
|
mcpAuthToken: "cap-token",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.mcpServers).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -30,6 +30,12 @@ export function withRuntimePaseoMcpServer(params: {
|
|||||||
config: AgentSessionConfig;
|
config: AgentSessionConfig;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
mcpBaseUrl: string | null;
|
mcpBaseUrl: string | null;
|
||||||
|
/**
|
||||||
|
* Capability token authenticating the injected connection to the daemon's
|
||||||
|
* Agent MCP endpoint. The daemon password is gated off this route, so without
|
||||||
|
* this header the agent's MCP requests are rejected when a password is set.
|
||||||
|
*/
|
||||||
|
mcpAuthToken: string | null;
|
||||||
}): AgentSessionConfig {
|
}): AgentSessionConfig {
|
||||||
const storedConfig = stripInternalPaseoMcpServer(params.config);
|
const storedConfig = stripInternalPaseoMcpServer(params.config);
|
||||||
if (!params.mcpBaseUrl || storedConfig.mcpServers?.[PASEO_MCP_SERVER_NAME]) {
|
if (!params.mcpBaseUrl || storedConfig.mcpServers?.[PASEO_MCP_SERVER_NAME]) {
|
||||||
@@ -42,6 +48,9 @@ export function withRuntimePaseoMcpServer(params: {
|
|||||||
[PASEO_MCP_SERVER_NAME]: {
|
[PASEO_MCP_SERVER_NAME]: {
|
||||||
type: "http",
|
type: "http",
|
||||||
url: `${params.mcpBaseUrl}?callerAgentId=${params.agentId}`,
|
url: `${params.mcpBaseUrl}?callerAgentId=${params.agentId}`,
|
||||||
|
...(params.mcpAuthToken
|
||||||
|
? { headers: { Authorization: `Bearer ${params.mcpAuthToken}` } }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
...storedConfig.mcpServers,
|
...storedConfig.mcpServers,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
extractWsBearerProtocol,
|
extractWsBearerProtocol,
|
||||||
extractWsBearerToken,
|
extractWsBearerToken,
|
||||||
hashDaemonPassword,
|
hashDaemonPassword,
|
||||||
|
isAgentMcpRequestAuthorized,
|
||||||
isBearerTokenValidAsync,
|
isBearerTokenValidAsync,
|
||||||
isBearerTokenValid,
|
isBearerTokenValid,
|
||||||
shouldBypassBearerAuth,
|
shouldBypassBearerAuth,
|
||||||
@@ -61,8 +62,62 @@ describe("daemon bearer validator", () => {
|
|||||||
expect(shouldBypassBearerAuth("GET", "/api/health")).toBe(true);
|
expect(shouldBypassBearerAuth("GET", "/api/health")).toBe(true);
|
||||||
// Guarded by its own single-use download token, not the daemon password.
|
// Guarded by its own single-use download token, not the daemon password.
|
||||||
expect(shouldBypassBearerAuth("GET", "/api/files/download")).toBe(true);
|
expect(shouldBypassBearerAuth("GET", "/api/files/download")).toBe(true);
|
||||||
|
// Guarded by its own per-daemon-run capability token (see
|
||||||
|
// isAgentMcpRequestAuthorized), not the daemon password.
|
||||||
|
expect(shouldBypassBearerAuth("POST", "/mcp/agents")).toBe(true);
|
||||||
// Everything else stays behind the daemon password.
|
// Everything else stays behind the daemon password.
|
||||||
expect(shouldBypassBearerAuth("GET", "/api/status")).toBe(false);
|
expect(shouldBypassBearerAuth("GET", "/api/status")).toBe(false);
|
||||||
expect(shouldBypassBearerAuth("POST", "/api/files/upload")).toBe(false);
|
expect(shouldBypassBearerAuth("POST", "/api/files/upload")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("agent MCP request authorizer", () => {
|
||||||
|
const CAPABILITY_TOKEN = "cap-token-abc123";
|
||||||
|
|
||||||
|
test("allows any request when no daemon password is configured", async () => {
|
||||||
|
expect(
|
||||||
|
await isAgentMcpRequestAuthorized({
|
||||||
|
password: undefined,
|
||||||
|
capabilityToken: CAPABILITY_TOKEN,
|
||||||
|
authorizationHeader: undefined,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts the injected capability token", async () => {
|
||||||
|
expect(
|
||||||
|
await isAgentMcpRequestAuthorized({
|
||||||
|
password: CORRECT_PASSWORD_HASH,
|
||||||
|
capabilityToken: CAPABILITY_TOKEN,
|
||||||
|
authorizationHeader: `Bearer ${CAPABILITY_TOKEN}`,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("still accepts a valid daemon-password bearer", async () => {
|
||||||
|
expect(
|
||||||
|
await isAgentMcpRequestAuthorized({
|
||||||
|
password: CORRECT_PASSWORD_HASH,
|
||||||
|
capabilityToken: CAPABILITY_TOKEN,
|
||||||
|
authorizationHeader: "Bearer correct-password",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects requests presenting neither the token nor a valid password", async () => {
|
||||||
|
expect(
|
||||||
|
await isAgentMcpRequestAuthorized({
|
||||||
|
password: CORRECT_PASSWORD_HASH,
|
||||||
|
capabilityToken: CAPABILITY_TOKEN,
|
||||||
|
authorizationHeader: undefined,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
await isAgentMcpRequestAuthorized({
|
||||||
|
password: CORRECT_PASSWORD_HASH,
|
||||||
|
capabilityToken: CAPABILITY_TOKEN,
|
||||||
|
authorizationHeader: "Bearer wrong-token",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { compare, compareSync, hashSync } from "bcryptjs";
|
import { compare, compareSync, hashSync } from "bcryptjs";
|
||||||
|
import { timingSafeEqual } from "node:crypto";
|
||||||
import type { RequestHandler } from "express";
|
import type { RequestHandler } from "express";
|
||||||
|
|
||||||
export const DAEMON_PASSWORD_BCRYPT_COST = 12;
|
export const DAEMON_PASSWORD_BCRYPT_COST = 12;
|
||||||
@@ -132,6 +133,16 @@ const BEARER_AUTH_BYPASS_PATHS = new Set([
|
|||||||
// rejects requests without a valid token (400/403), so dropping the bearer
|
// rejects requests without a valid token (400/403), so dropping the bearer
|
||||||
// here does not make the route unauthenticated.
|
// here does not make the route unauthenticated.
|
||||||
"/api/files/download",
|
"/api/files/download",
|
||||||
|
// The daemon injects its own agents' Paseo MCP connections at this endpoint
|
||||||
|
// (and connects its own per-client MCP client here). Those connections cannot
|
||||||
|
// carry the daemon password — it is only known in plaintext when set via env,
|
||||||
|
// never when set via the app — so the route authenticates them with a
|
||||||
|
// per-daemon-run capability token instead (see isAgentMcpRequestAuthorized).
|
||||||
|
// The token is injected only into local agent configs/sessions and never sent
|
||||||
|
// to remote clients, and the route still rejects callers presenting neither
|
||||||
|
// the token nor a valid daemon password, so dropping the global bearer here
|
||||||
|
// does not make the endpoint unauthenticated.
|
||||||
|
"/mcp/agents",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function shouldBypassBearerAuth(method: string, path: string): boolean {
|
export function shouldBypassBearerAuth(method: string, path: string): boolean {
|
||||||
@@ -140,3 +151,32 @@ export function shouldBypassBearerAuth(method: string, path: string): boolean {
|
|||||||
}
|
}
|
||||||
return BEARER_AUTH_BYPASS_PATHS.has(path);
|
return BEARER_AUTH_BYPASS_PATHS.has(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorizes a request to the Agent MCP endpoint (/mcp/agents), which is exempt
|
||||||
|
* from the global daemon-password middleware. Accepts either the per-daemon-run
|
||||||
|
* capability token the daemon injects into its own agents' configs and MCP
|
||||||
|
* client, or a valid daemon-password bearer (so existing password-authenticated
|
||||||
|
* callers keep working). When no daemon password is configured the endpoint is
|
||||||
|
* open, matching the global middleware's behavior.
|
||||||
|
*/
|
||||||
|
export async function isAgentMcpRequestAuthorized(input: {
|
||||||
|
password: string | undefined;
|
||||||
|
capabilityToken: string | null;
|
||||||
|
authorizationHeader: string | undefined;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
if (!input.password) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const token = extractHttpBearerToken(input.authorizationHeader);
|
||||||
|
if (input.capabilityToken !== null && token !== null) {
|
||||||
|
// Constant-time compare; length-guard first because timingSafeEqual throws
|
||||||
|
// on differing buffer lengths.
|
||||||
|
const provided = Buffer.from(token);
|
||||||
|
const expected = Buffer.from(input.capabilityToken);
|
||||||
|
if (provided.length === expected.length && timingSafeEqual(provided, expected)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isBearerTokenValidAsync({ password: input.password, token });
|
||||||
|
}
|
||||||
|
|||||||
@@ -132,7 +132,11 @@ import { ScriptHealthMonitor } from "./script-health-monitor.js";
|
|||||||
import { createScriptStatusEmitter } from "./script-status-projection.js";
|
import { createScriptStatusEmitter } from "./script-status-projection.js";
|
||||||
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
|
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
|
||||||
import { isHostnameAllowed, type HostnamesConfig } from "./hostnames.js";
|
import { isHostnameAllowed, type HostnamesConfig } from "./hostnames.js";
|
||||||
import { createRequireBearerMiddleware, type DaemonAuthConfig } from "./auth.js";
|
import {
|
||||||
|
createRequireBearerMiddleware,
|
||||||
|
isAgentMcpRequestAuthorized,
|
||||||
|
type DaemonAuthConfig,
|
||||||
|
} from "./auth.js";
|
||||||
|
|
||||||
type AgentMcpTransportMap = Map<string, StreamableHTTPServerTransport>;
|
type AgentMcpTransportMap = Map<string, StreamableHTTPServerTransport>;
|
||||||
|
|
||||||
@@ -330,6 +334,15 @@ export async function createPaseoDaemon(
|
|||||||
ttlMs: downloadTokenTtlMs,
|
ttlMs: downloadTokenTtlMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Capability token authenticating the daemon's own agents to the loopback
|
||||||
|
// Agent MCP endpoint (/mcp/agents). Random per daemon run, injected only into
|
||||||
|
// local agent configs and the daemon's own MCP client — never sent to remote
|
||||||
|
// clients — so it cannot be replayed off-box. This lets the injected MCP
|
||||||
|
// authenticate even when the daemon password is set via the app (hash only,
|
||||||
|
// no plaintext available). Mirrors the /api/files/download capability-token
|
||||||
|
// pattern.
|
||||||
|
const agentMcpAuthToken = randomUUID();
|
||||||
|
|
||||||
const listenTarget = parseListenString(config.listen);
|
const listenTarget = parseListenString(config.listen);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -555,6 +568,7 @@ export async function createPaseoDaemon(
|
|||||||
onWorkspaceStateMayHaveChanged: ({ cwd }) => {
|
onWorkspaceStateMayHaveChanged: ({ cwd }) => {
|
||||||
workspaceGitService.onWorkspaceStateMayHaveChanged(cwd);
|
workspaceGitService.onWorkspaceStateMayHaveChanged(cwd);
|
||||||
},
|
},
|
||||||
|
mcpAuthToken: agentMcpAuthToken,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -805,6 +819,20 @@ export async function createPaseoDaemon(
|
|||||||
req: express.Request,
|
req: express.Request,
|
||||||
res: express.Response,
|
res: express.Response,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
|
// This route is exempt from the global daemon-password middleware, so it
|
||||||
|
// authenticates here using the injected capability token (or a valid
|
||||||
|
// daemon password). Without this, a password-protected daemon would be
|
||||||
|
// wide open on its agent control plane.
|
||||||
|
if (
|
||||||
|
!(await isAgentMcpRequestAuthorized({
|
||||||
|
password: config.auth?.password,
|
||||||
|
capabilityToken: agentMcpAuthToken,
|
||||||
|
authorizationHeader: req.header("authorization"),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
res.status(401).json({ error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (config.mcpDebug) {
|
if (config.mcpDebug) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1220,7 +1220,13 @@ export class Session {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const transport = new StreamableHTTPClientTransport(new URL(this.mcpBaseUrl));
|
const authToken = this.agentManager.getMcpAuthToken();
|
||||||
|
const transport = new StreamableHTTPClientTransport(
|
||||||
|
new URL(this.mcpBaseUrl),
|
||||||
|
authToken
|
||||||
|
? { requestInit: { headers: { Authorization: `Bearer ${authToken}` } } }
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
this.agentMcpClient = await experimental_createMCPClient({
|
this.agentMcpClient = await experimental_createMCPClient({
|
||||||
transport,
|
transport,
|
||||||
|
|||||||
Reference in New Issue
Block a user