From 198a0152ce712d070c54c54575503e4f5fc690fc Mon Sep 17 00:00:00 2001 From: a8trejo Date: Tue, 11 Aug 2026 10:29:22 -0700 Subject: [PATCH 1/6] [Feat] Adding diff view for editing environments --- apps/web/package.json | 6 + .../environments/EditEnvironmentPage.tsx | 33 +++ .../environments/EnvironmentDiffMergeView.tsx | 163 ++++++++++++++ .../environments/YamlEnvironmentEditor.tsx | 69 +++++- pnpm-lock.yaml | 201 +++++++++++++++++- 5 files changed, 464 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/components/settings/environments/EnvironmentDiffMergeView.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 4b91d3039..a78cb9220 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -24,6 +24,10 @@ "@aws-sdk/client-s3": "3.1000.0", "@aws-sdk/s3-request-presigner": "3.1000.0", "@better-auth/drizzle-adapter": "^1.6.22", + "@codemirror/lang-yaml": "^6.0.0", + "@codemirror/merge": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", "@hookform/resolvers": "^5.2.2", "@linear/sdk": "^68.0.0", "@melloware/react-logviewer": "^6.4.1", @@ -74,6 +78,7 @@ "@trpc/client": "^11.15.0", "@trpc/server": "^11.15.0", "@trpc/tanstack-react-query": "^11.15.0", + "@uiw/codemirror-theme-github": "^4.25.11", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", @@ -86,6 +91,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "codemirror": "^6.0.0", "cron-parser": "5.6.1", "cronstrue": "^3.24.0", "date-fns": "^4.1.0", diff --git a/apps/web/src/components/settings/environments/EditEnvironmentPage.tsx b/apps/web/src/components/settings/environments/EditEnvironmentPage.tsx index c843b9e57..92dd34c96 100644 --- a/apps/web/src/components/settings/environments/EditEnvironmentPage.tsx +++ b/apps/web/src/components/settings/environments/EditEnvironmentPage.tsx @@ -40,6 +40,7 @@ import { type SelectedRepositorySummary } from './EnvironmentDefinitionAgentTask import { EnvironmentRepositorySelector } from './EnvironmentRepositorySelector'; import { UpdateGitHubReposHint } from './UpdateGitHubReposHint'; import { + type YamlEditorTab, type YamlEnvironmentEditorHandle, YamlEnvironmentEditor, } from './YamlEnvironmentEditor'; @@ -87,6 +88,9 @@ export function EditEnvironmentPage({ null, ); const [editorResetKey, setEditorResetKey] = useState(0); + // Lifted above YamlMasterView/YamlEnvironmentEditor so it survives the + // `editorResetKey`-driven remount that happens when switching versions. + const [yamlEditorTab, setYamlEditorTab] = useState('editor'); const [selectedVersionValue, setSelectedVersionValue] = useState( CURRENT_VERSION_VALUE, ); @@ -121,6 +125,7 @@ export function EditEnvironmentPage({ setAgentChangeRequest(''); setSelectedModelId(undefined); setIsLoadingVersion(false); + setYamlEditorTab('editor'); }, [environmentId]); useEffect(() => { @@ -149,6 +154,7 @@ export function EditEnvironmentPage({ setAgentChangeRequest(''); setSelectedModelId(undefined); setActiveView('yaml'); + setYamlEditorTab('editor'); }; const resetLoadedVersionState = (shouldResetEditor: boolean) => { @@ -329,6 +335,17 @@ export function EditEnvironmentPage({ [configVersionsQuery.data], ); const showVersionSelector = versionOptions.length >= 2; + // `listConfigVersions` is sorted newest-first, so the first entry is the + // version number of the currently saved config. + const currentVersionNumber = configVersionsQuery.data?.[0]?.version; + const diffOriginalLabel = + currentVersionNumber != null + ? `Saved configuration (Version ${currentVersionNumber})` + : undefined; + const diffModifiedLabel = + selectedVersionValue !== CURRENT_VERSION_VALUE + ? `Editing Version ${selectedVersionValue}` + : undefined; return ( <> @@ -406,6 +423,10 @@ export function EditEnvironmentPage({ versionOptions={versionOptions} isLoadingVersion={isLoadingVersion} onSelectVersion={(value) => void handleVersionSelect(value)} + activeTab={yamlEditorTab} + onActiveTabChange={setYamlEditorTab} + diffOriginalLabel={diffOriginalLabel} + diffModifiedLabel={diffModifiedLabel} /> ) : ( ; initialConfig: EnvironmentConfig | undefined; @@ -477,6 +502,10 @@ function YamlMasterView({ versionOptions: Array<{ value: string; label: string }>; isLoadingVersion: boolean; onSelectVersion: (value: string) => void; + activeTab: YamlEditorTab; + onActiveTabChange: (tab: YamlEditorTab) => void; + diffOriginalLabel?: string; + diffModifiedLabel?: string; }) { return (
@@ -491,6 +520,10 @@ function YamlMasterView({ isSaving={isSaving} warnings={warnings} hideActions + activeTab={activeTab} + onActiveTabChange={onActiveTabChange} + diffOriginalLabel={diffOriginalLabel} + diffModifiedLabel={diffModifiedLabel} />
diff --git a/apps/web/src/components/settings/environments/EnvironmentDiffMergeView.tsx b/apps/web/src/components/settings/environments/EnvironmentDiffMergeView.tsx new file mode 100644 index 000000000..a5d1c20a7 --- /dev/null +++ b/apps/web/src/components/settings/environments/EnvironmentDiffMergeView.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import { useTheme } from 'next-themes'; + +import { basicSetup, EditorView } from 'codemirror'; +import { EditorState } from '@codemirror/state'; +import { yaml } from '@codemirror/lang-yaml'; +import { MergeView } from '@codemirror/merge'; +import { githubDark, githubLight } from '@uiw/codemirror-theme-github'; + +import { Lock, Pencil } from '@/components/system'; +import { cn } from '@/lib/utils'; + +// githubLight/githubDark already provide full editor chrome (background, +// gutters, selection, syntax colors) matching the same GitHub palette the +// shiki-based code blocks elsewhere in the app use. +const fontTheme = EditorView.theme({ + '&': { height: '100%' }, + '.cm-scroller': { + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', + fontSize: '0.8rem', + }, +}); + +function sideExtensions(dark: boolean) { + return [basicSetup, yaml(), fontTheme, dark ? githubDark : githubLight]; +} + +interface EnvironmentDiffMergeViewProps { + /** Saved/baseline YAML, shown read-only on the left. */ + original: string; + /** Editable draft YAML, shown on the right. */ + modified: string; + onModifiedChange: (value: string) => void; + className?: string; + /** Label for the left (read-only) pane. Defaults to "Saved configuration". */ + originalLabel?: string; + /** Label for the right (editable) pane. Defaults to "Editing". */ + modifiedLabel?: string; +} + +export function EnvironmentDiffMergeView({ + original, + modified, + onModifiedChange, + className, + originalLabel = 'Saved configuration', + modifiedLabel = 'Editing', +}: EnvironmentDiffMergeViewProps) { + const containerRef = useRef(null); + const mergeViewRef = useRef(null); + const lastEmittedRef = useRef(modified); + const onModifiedChangeRef = useRef(onModifiedChange); + onModifiedChangeRef.current = onModifiedChange; + + const { resolvedTheme } = useTheme(); + const dark = resolvedTheme === 'dark'; + + useEffect(() => { + if (!containerRef.current) { + return; + } + + const mergeView = new MergeView({ + parent: containerRef.current, + a: { + doc: original, + extensions: [...sideExtensions(dark), EditorState.readOnly.of(true)], + }, + b: { + doc: modified, + extensions: [ + ...sideExtensions(dark), + EditorView.updateListener.of((update) => { + if (!update.docChanged) { + return; + } + + const value = update.state.doc.toString(); + lastEmittedRef.current = value; + onModifiedChangeRef.current(value); + }), + ], + }, + gutter: true, + highlightChanges: true, + }); + + mergeViewRef.current = mergeView; + + return () => { + mergeView.destroy(); + mergeViewRef.current = null; + }; + // A theme change is the only prop that should tear down and rebuild the whole view; + // `original`/`modified` updates are pushed into the live view by the effects below instead, + // so they don't clobber in-progress edits or cursor position on every keystroke. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dark]); + + useEffect(() => { + const mergeView = mergeViewRef.current; + + if (!mergeView) { + return; + } + + const currentA = mergeView.a.state.doc.toString(); + + if (currentA !== original) { + mergeView.a.dispatch({ + changes: { from: 0, to: currentA.length, insert: original }, + }); + } + }, [original]); + + useEffect(() => { + const mergeView = mergeViewRef.current; + + if (!mergeView || modified === lastEmittedRef.current) { + return; + } + + const currentB = mergeView.b.state.doc.toString(); + + if (currentB !== modified) { + mergeView.b.dispatch({ + changes: { from: 0, to: currentB.length, insert: modified }, + }); + } + + lastEmittedRef.current = modified; + }, [modified]); + + return ( +
+
+
+ + {originalLabel} +
+
+ + {modifiedLabel} +
+
+
+
+ ); +} diff --git a/apps/web/src/components/settings/environments/YamlEnvironmentEditor.tsx b/apps/web/src/components/settings/environments/YamlEnvironmentEditor.tsx index cd2f56b2f..421fccb1d 100644 --- a/apps/web/src/components/settings/environments/YamlEnvironmentEditor.tsx +++ b/apps/web/src/components/settings/environments/YamlEnvironmentEditor.tsx @@ -4,6 +4,7 @@ import { useState, useCallback, useEffect, + useMemo, forwardRef, useImperativeHandle, } from 'react'; @@ -13,6 +14,7 @@ import { AlertTriangle, Check, FileCode, + FileDiffIcon, Eye, Settings2, } from '@/components/system'; @@ -35,6 +37,7 @@ import { } from '@/components/system'; import { cn } from '@/lib/utils'; +import { EnvironmentDiffMergeView } from './EnvironmentDiffMergeView'; import { EnvironmentPreviewContent } from './EnvironmentPreview'; import { useRepositories } from '@/hooks/source-control'; import { configToYaml } from './yaml-utils'; @@ -153,6 +156,8 @@ export interface YamlEnvironmentEditorHandle { save: () => Promise; } +export type YamlEditorTab = 'editor' | 'yaml' | 'diff' | 'preview'; + interface YamlEnvironmentEditorProps { initialConfig?: EnvironmentConfig; initialYamlContent?: string; @@ -165,6 +170,16 @@ interface YamlEnvironmentEditorProps { mode: 'create' | 'edit'; hideActions?: boolean; warnings?: string[]; + /** + * Optionally lift the active sub-tab to the parent so it survives a + * version-select remount (the parent bumps a `key` on this component to + * reset editor state, which would otherwise reset the tab too). + */ + activeTab?: YamlEditorTab; + onActiveTabChange?: (tab: YamlEditorTab) => void; + /** Diff tab pane labels; see EnvironmentDiffMergeView for defaults. */ + diffOriginalLabel?: string; + diffModifiedLabel?: string; } export const YamlEnvironmentEditor = forwardRef< @@ -181,14 +196,21 @@ export const YamlEnvironmentEditor = forwardRef< mode, hideActions = false, warnings: externalWarnings, + activeTab: controlledActiveTab, + onActiveTabChange, + diffOriginalLabel, + diffModifiedLabel, }, ref, ) { const [yamlContent, setYamlContent] = useState(''); const [validation, setValidation] = useState(null); - const [activeTab, setActiveTab] = useState<'editor' | 'yaml' | 'preview'>( + const [internalActiveTab, setInternalActiveTab] = useState( mode === 'edit' ? 'editor' : 'yaml', ); + const activeTab = controlledActiveTab ?? internalActiveTab; + const setActiveTab = onActiveTabChange ?? setInternalActiveTab; + const [editorConfig, setEditorConfig] = useState( initialConfig ?? null, ); @@ -262,10 +284,10 @@ export const YamlEnvironmentEditor = forwardRef< setActiveTab('yaml'); } } else { - setActiveTab('yaml'); + setActiveTab(tab === 'diff' ? 'diff' : 'yaml'); } }, - [validate], + [validate, setActiveTab], ); const handleSave = useCallback(async () => { @@ -283,7 +305,7 @@ export const YamlEnvironmentEditor = forwardRef< if (!saveResult.success) { setSaveError(saveResult.error || 'Failed to save environment'); } - }, [validate, onSave]); + }, [validate, onSave, setActiveTab]); const handleYamlChange = useCallback( (value: string) => { @@ -331,7 +353,16 @@ export const YamlEnvironmentEditor = forwardRef< setHasUserEdited(false); setValidation(null); setSaveError(null); - setActiveTab('yaml'); + + // Keep the Editor/Preview tabs in sync too, in case one of them is + // already active when a different version loads. + try { + const parsed = YAML.parse(initialYamlContent); + const result = environmentConfigSchema.safeParse(parsed); + setEditorConfig(result.success ? result.data : null); + } catch { + setEditorConfig(null); + } }, [initialYamlContent]); // Only reset content from the stored environment config if the user hasn't @@ -353,6 +384,14 @@ export const YamlEnvironmentEditor = forwardRef< save: handleSave, })); + // Baseline for the Diff tab: the last saved configuration, compared + // against whatever's currently in the editor (an in-progress draft, or a + // loaded past version). + const savedYaml = useMemo( + () => (initialConfig ? configToYaml(initialConfig) : ''), + [initialConfig], + ); + return (
@@ -369,6 +408,12 @@ export const YamlEnvironmentEditor = forwardRef< Yaml + {mode === 'edit' ? ( + + + Diff + + ) : null} Preview @@ -452,6 +497,20 @@ export const YamlEnvironmentEditor = forwardRef< )} + {/* Diff Tab */} + {mode === 'edit' ? ( + + + + ) : null} + {/* Preview Tab */} {validation?.data ? ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc96ff541..d07b22760 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -528,6 +528,18 @@ importers: '@better-auth/drizzle-adapter': specifier: ^1.6.22 version: 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8)) + '@codemirror/lang-yaml': + specifier: ^6.0.0 + version: 6.1.3 + '@codemirror/merge': + specifier: ^6.0.0 + version: 6.12.2 + '@codemirror/state': + specifier: ^6.0.0 + version: 6.7.1 + '@codemirror/view': + specifier: ^6.0.0 + version: 6.43.7 '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.60.0(react@19.2.4)) @@ -678,6 +690,9 @@ importers: '@trpc/tanstack-react-query': specifier: ^11.15.0 version: 11.15.0(@tanstack/react-query@5.90.20(react@19.2.4))(@trpc/client@11.15.0(@trpc/server@11.15.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.15.0(typescript@5.9.3))(react@19.2.4)(typescript@5.9.3) + '@uiw/codemirror-theme-github': + specifier: ^4.25.11 + version: 4.25.11(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7) '@xterm/addon-fit': specifier: ^0.11.0 version: 0.11.0 @@ -714,6 +729,9 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + codemirror: + specifier: ^6.0.0 + version: 6.0.2 cron-parser: specifier: 5.6.1 version: 5.6.1 @@ -2389,6 +2407,33 @@ packages: '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + + '@codemirror/lang-yaml@6.1.3': + resolution: {integrity: sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/merge@6.12.2': + resolution: {integrity: sha512-V8JvyAPjHbPupqP7BeMcsdsYCbyPij74jxIbaIJDORI+VZzW44zFmon8bF+oxGWvOKhcRmkiUMXd8MxHr3YA2w==} + + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} + + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + + '@codemirror/view@6.43.7': + resolution: {integrity: sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} @@ -2517,11 +2562,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} @@ -3053,6 +3098,18 @@ packages: '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@lezer/yaml@1.0.4': + resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} + '@libsql/client-wasm@0.15.5': resolution: {integrity: sha512-JPjnGnLGQu36SPDskXSgyLoVBA0/IgcEC52MKnIa7/rKGIY1I4WKMIYrl2hkbw7+xYzWrGk6Vr8AZ9PeNEtgVA==} bundledDependencies: @@ -3075,6 +3132,9 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -6184,6 +6244,16 @@ packages: resolution: {integrity: sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==} engines: {node: '>=20.0.0'} + '@uiw/codemirror-theme-github@4.25.11': + resolution: {integrity: sha512-3s0LK3gX2mvGI996z3G0tEHZqshbeJRly+QMRsKGAI1Tfr1V475bfaW9NvnoDdINuQHt+RB+ri6q57+WWF8d+A==} + + '@uiw/codemirror-themes@4.25.11': + resolution: {integrity: sha512-SBNCOgRsCtewGNocRbmjbCkltGXlFcPJsvhxQ351VynQjnWUiPbUrFcEU/haQ3HanROdAAjWXZJPk5bMBxl2jw==} + peerDependencies: + '@codemirror/language': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} @@ -6948,6 +7018,9 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + codsen-utils@1.7.3: resolution: {integrity: sha512-YIFQQ1n2NSgwoB3sCe7RpkZzsrPxTMek6jc7wC9fXOm1wwfWAKja9gLOMEjlXOUd3LKV3o6Jci7n9BoHs5Z8Sg==} engines: {node: '>=14.18.0'} @@ -7101,6 +7174,9 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -10859,6 +10935,9 @@ packages: strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -11113,6 +11192,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -11492,6 +11572,9 @@ packages: vscode-languageserver-types@3.18.0: resolution: {integrity: sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -11626,7 +11709,7 @@ packages: engines: {node: '>=20'} xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz: - resolution: {integrity: sha512-+nKZ39+nvK7Qq6i0PvWWRA4j/EkfWOtkP/YhMtupm+lJIiHxUrgTr1CcKv1nBk1rHtkRRQ3O2+Ih/q/sA+FXZA==, tarball: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz} + resolution: {tarball: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz} version: 0.20.2 engines: {node: '>=0.8'} hasBin: true @@ -12854,6 +12937,70 @@ snapshots: '@chevrotain/types@11.1.2': {} + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.10.4': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + '@lezer/common': 1.5.2 + + '@codemirror/lang-yaml@6.1.3': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/yaml': 1.0.4 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + crelt: 1.0.7 + + '@codemirror/merge@6.12.2': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + '@lezer/highlight': 1.2.3 + style-mod: 4.1.3 + + '@codemirror/search@6.7.1': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + crelt: 1.0.7 + + '@codemirror/state@6.7.1': + dependencies: + '@marijn/find-cluster-break': 1.0.3 + + '@codemirror/view@6.43.7': + dependencies: + '@codemirror/state': 6.7.1 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@colors/colors@1.6.0': {} '@connectrpc/connect-web@2.0.0-rc.3(@bufbuild/protobuf@2.12.1)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.12.1))': @@ -13484,6 +13631,22 @@ snapshots: '@jsdevtools/ono@7.1.3': {} + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/yaml@1.0.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@libsql/client-wasm@0.15.5': dependencies: '@libsql/core': 0.15.15 @@ -13518,6 +13681,8 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@marijn/find-cluster-break@1.0.3': {} + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.9 @@ -16688,6 +16853,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@uiw/codemirror-theme-github@4.25.11(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7)': + dependencies: + '@uiw/codemirror-themes': 4.25.11(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7) + transitivePeerDependencies: + - '@codemirror/language' + - '@codemirror/state' + - '@codemirror/view' + + '@uiw/codemirror-themes@4.25.11(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7)': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + '@ungap/structured-clone@1.3.1': {} '@upsetjs/venn.js@2.0.0': @@ -17514,6 +17693,16 @@ snapshots: - '@types/react' - '@types/react-dom' + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + codsen-utils@1.7.3: dependencies: rfdc: 1.4.1 @@ -17633,6 +17822,8 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 + crelt@1.0.7: {} + cron-parser@4.9.0: dependencies: luxon: 3.7.2 @@ -22228,6 +22419,8 @@ snapshots: strnum@2.2.3: {} + style-mod@4.1.3: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -22933,6 +23126,8 @@ snapshots: vscode-languageserver-types@3.18.0: {} + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 From e4db83af95470ae3581444405f70e0ce549ceb4d Mon Sep 17 00:00:00 2001 From: a8trejo Date: Tue, 11 Aug 2026 10:31:09 -0700 Subject: [PATCH 2/6] [Fix] crash-loop of services when pm2 resolves wrong node version --- scripts/watchman.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/watchman.sh b/scripts/watchman.sh index 5176636c2..77fbfd9bc 100755 --- a/scripts/watchman.sh +++ b/scripts/watchman.sh @@ -30,8 +30,15 @@ if command -v watchman &>/dev/null && watchman version &>/dev/null; then trap cleanup EXIT INT TERM + # Capture PATH so build/trigger/runtime commands can find pnpm/node/etc. + # `bash -lc` starts a login shell, which re-sources bash's own profile + # instead of inheriting this PATH (mise/nodenv setup living in the + # invoking shell's own dotfiles, e.g. zsh, is not picked up), so every + # `bash -lc` call below must re-export it explicitly. + CURRENT_PATH="$PATH" + echo "[$SERVICE_NAME:watchman] Initial build..." - bash -lc "$BUILD_CMD" + bash -lc "export PATH='$CURRENT_PATH' && $BUILD_CMD" # Use watchman trigger for reliable file-change detection. # watchman-make subscriptions stall under PM2's piped stdio. @@ -40,9 +47,6 @@ if command -v watchman &>/dev/null && watchman version &>/dev/null; then # Remove any stale trigger first watchman -- trigger-del "$REPO_ROOT" "$TRIGGER_NAME" >/dev/null 2>&1 || true - # Capture PATH so the trigger command can find pnpm/node/etc. - CURRENT_PATH="$PATH" - echo "[$SERVICE_NAME:watchman] Ensuring watchman is watching $REPO_ROOT..." watchman watch-project "$REPO_ROOT" >/dev/null 2>&1 @@ -62,7 +66,7 @@ if command -v watchman &>/dev/null && watchman version &>/dev/null; then WATCHMAN_CMD echo "[$SERVICE_NAME:watchman] Starting runtime..." - exec bash -lc "$RUN_CMD" + exec bash -lc "export PATH='$CURRENT_PATH' && $RUN_CMD" fi if command -v inotifywait &>/dev/null; then @@ -71,7 +75,7 @@ if command -v inotifywait &>/dev/null; then CURRENT_PATH="$PATH" echo "[$SERVICE_NAME:inotifywait] Initial build..." - bash -lc "$BUILD_CMD" + bash -lc "export PATH='$CURRENT_PATH' && $BUILD_CMD" echo "[$SERVICE_NAME:inotifywait] Starting runtime..." bash -c "export PATH='$CURRENT_PATH' && cd '$APP_DIR' && $RUN_CMD" & From 932e55c26d0d4b43731517ff20fb951cde3fcf01 Mon Sep 17 00:00:00 2001 From: a8trejo Date: Tue, 11 Aug 2026 11:13:28 -0700 Subject: [PATCH 3/6] Fix knip feedback --- apps/web/package.json | 1 - pnpm-lock.yaml | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index a78cb9220..ee8c1321c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -27,7 +27,6 @@ "@codemirror/lang-yaml": "^6.0.0", "@codemirror/merge": "^6.0.0", "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", "@hookform/resolvers": "^5.2.2", "@linear/sdk": "^68.0.0", "@melloware/react-logviewer": "^6.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d07b22760..dc62a88f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,7 +240,7 @@ importers: version: 8.5.1(jiti@2.7.0)(postcss@8.5.23)(tsx@4.20.4)(typescript@5.9.3)(yaml@2.9.0) vitest: specifier: ^4.1.1 - version: 4.1.1(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0)) + version: 4.1.1(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0)) apps/bullmq: dependencies: @@ -368,7 +368,7 @@ importers: version: 8.5.1(jiti@2.7.0)(postcss@8.5.23)(tsx@4.20.4)(typescript@5.9.3)(yaml@2.9.0) vitest: specifier: ^4.1.1 - version: 4.1.1(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0)) + version: 4.1.1(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0)) apps/dev: dependencies: @@ -537,9 +537,6 @@ importers: '@codemirror/state': specifier: ^6.0.0 version: 6.7.1 - '@codemirror/view': - specifier: ^6.0.0 - version: 6.43.7 '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.60.0(react@19.2.4)) From a54708e77dd45bec62753feadf2fe42170e9bb12 Mon Sep 17 00:00:00 2001 From: a8trejo Date: Tue, 11 Aug 2026 11:24:45 -0700 Subject: [PATCH 4/6] Revert "[Fix] crash-loop of services when pm2 resolves wrong node version" This reverts commit e4db83af95470ae3581444405f70e0ce549ceb4d. --- scripts/watchman.sh | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/scripts/watchman.sh b/scripts/watchman.sh index 77fbfd9bc..5176636c2 100755 --- a/scripts/watchman.sh +++ b/scripts/watchman.sh @@ -30,15 +30,8 @@ if command -v watchman &>/dev/null && watchman version &>/dev/null; then trap cleanup EXIT INT TERM - # Capture PATH so build/trigger/runtime commands can find pnpm/node/etc. - # `bash -lc` starts a login shell, which re-sources bash's own profile - # instead of inheriting this PATH (mise/nodenv setup living in the - # invoking shell's own dotfiles, e.g. zsh, is not picked up), so every - # `bash -lc` call below must re-export it explicitly. - CURRENT_PATH="$PATH" - echo "[$SERVICE_NAME:watchman] Initial build..." - bash -lc "export PATH='$CURRENT_PATH' && $BUILD_CMD" + bash -lc "$BUILD_CMD" # Use watchman trigger for reliable file-change detection. # watchman-make subscriptions stall under PM2's piped stdio. @@ -47,6 +40,9 @@ if command -v watchman &>/dev/null && watchman version &>/dev/null; then # Remove any stale trigger first watchman -- trigger-del "$REPO_ROOT" "$TRIGGER_NAME" >/dev/null 2>&1 || true + # Capture PATH so the trigger command can find pnpm/node/etc. + CURRENT_PATH="$PATH" + echo "[$SERVICE_NAME:watchman] Ensuring watchman is watching $REPO_ROOT..." watchman watch-project "$REPO_ROOT" >/dev/null 2>&1 @@ -66,7 +62,7 @@ if command -v watchman &>/dev/null && watchman version &>/dev/null; then WATCHMAN_CMD echo "[$SERVICE_NAME:watchman] Starting runtime..." - exec bash -lc "export PATH='$CURRENT_PATH' && $RUN_CMD" + exec bash -lc "$RUN_CMD" fi if command -v inotifywait &>/dev/null; then @@ -75,7 +71,7 @@ if command -v inotifywait &>/dev/null; then CURRENT_PATH="$PATH" echo "[$SERVICE_NAME:inotifywait] Initial build..." - bash -lc "export PATH='$CURRENT_PATH' && $BUILD_CMD" + bash -lc "$BUILD_CMD" echo "[$SERVICE_NAME:inotifywait] Starting runtime..." bash -c "export PATH='$CURRENT_PATH' && cd '$APP_DIR' && $RUN_CMD" & From 7c5f70fcccff0f64483877f9b434f4afd6de8722 Mon Sep 17 00:00:00 2001 From: a8trejo Date: Tue, 11 Aug 2026 11:54:30 -0700 Subject: [PATCH 5/6] [Fix] yaml editor test --- .../environments/YamlEnvironmentEditor.client.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/web/src/components/settings/environments/YamlEnvironmentEditor.client.test.tsx b/apps/web/src/components/settings/environments/YamlEnvironmentEditor.client.test.tsx index d326040fd..b88f7a1a3 100644 --- a/apps/web/src/components/settings/environments/YamlEnvironmentEditor.client.test.tsx +++ b/apps/web/src/components/settings/environments/YamlEnvironmentEditor.client.test.tsx @@ -108,6 +108,8 @@ describe('YamlEnvironmentEditor', () => { initialYamlContent={configToYaml(currentConfig)} onSave={onSave} onCancel={() => {}} + activeTab="yaml" + onActiveTabChange={() => {}} />, ); @@ -127,6 +129,8 @@ describe('YamlEnvironmentEditor', () => { initialYamlContent={configToYaml(historicalConfig)} onSave={onSave} onCancel={() => {}} + activeTab="yaml" + onActiveTabChange={() => {}} />, ); From 96765d5db897d306563a6a29c6cf4a9e03c54237 Mon Sep 17 00:00:00 2001 From: a8trejo Date: Tue, 11 Aug 2026 14:30:35 -0700 Subject: [PATCH 6/6] docs(changeset): Added changeset file for environment diff view --- .changeset/four-needles-pull.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/four-needles-pull.md diff --git a/.changeset/four-needles-pull.md b/.changeset/four-needles-pull.md new file mode 100644 index 000000000..918fc9ea2 --- /dev/null +++ b/.changeset/four-needles-pull.md @@ -0,0 +1,5 @@ +--- +"@roomote/web": minor +--- + +Add a diff tab to the environment editor. Shows side by side merge view to compare new or existing versions