Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-ui-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
node-version: "22.x"
cache: npm

- name: Install dependencies
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ playground/
.turbo
styles.css
.aider*
app_templates/
local/
Comment on lines +29 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Anchor these ignore rules to the repo root.

app_templates/ currently matches any directory with that name, including src/writer/app_templates/, and because it comes after Lines 19-20 it can effectively override the existing allowlist intent there. local/ has the same overbroad matching behavior. If these are meant to ignore only top-level dev artifacts, make them root-anchored.

Proposed fix
-app_templates/
-local/
+/app_templates/
+/local/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app_templates/
local/
/app_templates/
/local/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 29 - 30, The .gitignore entries app_templates/ and
local/ are unanchored and match directories anywhere; update the patterns to be
root-anchored by changing the entries for "app_templates/" and "local/" to use a
leading slash (i.e., anchor "/app_templates/" and "/local/") so they only ignore
top-level directories and don't override earlier allowlist rules; modify the
existing patterns for app_templates and local in the .gitignore accordingly.

1,213 changes: 650 additions & 563 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,44 @@
"src/ui",
"tests/e2e"
],
"engines": {
"node": "22.x.x",
"npm": "10.x.x"
},
"engines": {
"node": "22.x.x",
"npm": "10.x.x"
},
Comment thread
bybash marked this conversation as resolved.
"scripts": {
"build": "npm run ui:build && npm run apps:build && npm run ui:codegen",
"test": "npm run --if-present -ws test",
"lint": "npm run --if-present -ws lint",
"dev": "npm run -w writer-ui dev",
"custom.dev": "npm run -w writer-ui custom.dev",

"cli:test": "pytest tests -o log_cli=true ",
"cli:lint": "mypy ./src/writer --exclude app_templates/* && ruff check",
"cli:build": "npm run ui:codegen",

"ui:codegen": "npm run -w writer-ui codegen",
"ui:codegen": "npm run -w writer-ui codegen",
"ui:dev": "npm run -w writer-ui dev",
"ui:build": "npm run -w writer-ui build",
"ui:preview": "npm run -w writer-ui preview",
"ui:custom.build": "npm run -w writer-ui custom.build",
"ui:custom.check": "npm run -w writer-ui custom.check",
"ui:lint": "npm run -w writer-ui lint",
"ui:lint.ci": "npm run -w writer-ui lint.ci",

"e2e": "npm run -w writer-e2e e2e",
"e2e:setup": "npm run -w writer-e2e e2e:setup",
"e2e:ui": "npm run -w writer-e2e e2e:ui",
"e2e:ci": "npm run -w writer-e2e e2e:ci",
"e2e:firefox": "npm run -w writer-e2e e2e:firefox",
"e2e:chromium": "npm run -w writer-e2e e2e:chromium",
"e2e:webkit": "npm run -w writer-e2e e2e:webkit",

"apps:build": "cp -R ./apps/hello ./src/writer/app_templates/ && cp -R ./apps/default ./src/writer/app_templates/",
"codegen": "npm run ui:codegen",
"ld:upload-sourcemaps": "./scripts/upload-sourcemaps.sh",
"build:with-sourcemaps": "npm run ui:build && npm run ld:upload-sourcemaps"
},
"devDependencies": {
"playwright": "^1.57.0"
},
"dependencies": {
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^24.2.0",
"vscode": "npm:@codingame/monaco-vscode-extension-api@^24.2.0"
}
}
765 changes: 760 additions & 5 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ writer-sdk = ">= 2.3.1, < 3"
python-multipart = ">=0.0.7, < 1"
orjson = "^3.11.0, <4"
launchdarkly-server-sdk = "^9.12.0"
python-lsp-server = {extras = ["all"], version = "^1.12.0"}

[tool.poetry.group.build]
optional = true
Expand Down
16 changes: 12 additions & 4 deletions src/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "NODE_OPTIONS=--max-old-space-size=8192 vite build",
"preview": "vite preview --port 5050",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path ../../.gitignore --ignore-path .gitignore",
"lint.ci": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path ../../.gitignore --ignore-path .gitignore",
Expand All @@ -18,10 +18,15 @@
"dependencies": {
"@apache-arrow/ts": "^15.0.2",
"@chamaeleonidae/chmln": "^1.0.1",
"@codingame/monaco-vscode-api": "^24.2.0",
"@codingame/monaco-vscode-languages-service-override": "^24.2.0",
"@codingame/monaco-vscode-textmate-service-override": "^24.2.0",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"@floating-ui/vue": "^1.1.5",
"@fontsource/poppins": "^5.0.14",
"@fullstory/browser": "^2.0.6",
"@googlemaps/js-api-loader": "^1.16.6",
"@launchdarkly/observability": "^0.4.0",
"@launchdarkly/session-replay": "^0.4.5",
"@monaco-editor/loader": "^1.3.3",
"@tato30/vue-pdf": "^1.11.3",
"@tiptap/extension-document": "^3.0.7",
Expand All @@ -36,19 +41,22 @@
"arquero": "^5.2.0",
"attr-accept": "^2.2.5",
"chroma-js": "^2.4.2",
"@launchdarkly/observability": "^0.4.0",
"@launchdarkly/session-replay": "^0.4.5",
"fast-uri": "^3.1.0",
"launchdarkly-js-client-sdk": "^3.8.1",
"lucide": "^0.525.0",
"mapbox-gl": "^3.2.0",
"marked": "^12.0.1",
"monaco-editor": "^0.47.0",
"monaco-editor": "^0.52.0",
"monaco-languageclient": "^10.5.0",
"monacopilot": "^1.2.9",
"plotly.js-dist-min": "^2.35.2",
"pretty-bytes": "^6.1.1",
"typescript": "^5.4.3",
"vega": "^5.22.1",
"vega-embed": "^6.22.1",
"vega-lite": "^5.7.1",
"vscode-languageclient": "^9.0.1",
"vscode-ws-jsonrpc": "^3.5.0",
"vue": "^3.5.0",
"vue-dompurify-html": "^5.0.1"
},
Expand Down
4 changes: 4 additions & 0 deletions src/ui/src/builder/BuilderApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ import { defineAsyncComponentWithLoader } from "@/utils/defineAsyncComponentWith
import BuilderAppSocketTimeoutModal from "./BuilderAppSocketTimeoutModal.vue";
import { useSocketTimeout } from "./useSocketTimeout";
import BlueprintsNavigationStack from "@/components/blueprints/BlueprintsNavigationStack.vue";
import { setupCodeEditorSettings } from "@/composables/useCodeEditorSettings";

// Setup code editor settings once at the root level
setupCodeEditorSettings();

provide(injectionKeys.isAutogenModalShown, ref(false));

Expand Down
188 changes: 168 additions & 20 deletions src/ui/src/builder/BuilderEmbeddedCodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
ref="rootEl"
class="BuilderEmbeddedCodeEditor"
:class="{
'BuilderEmbeddedCodeEditor--full': variant === 'full',
'BuilderEmbeddedCodeEditor--halfScreen': variant === 'half-screen',
'BuilderEmbeddedCodeEditor--singleLine': variant === 'single-line',
'BuilderEmbeddedCodeEditor--full': props.variant === 'full',
'BuilderEmbeddedCodeEditor--halfScreen':
props.variant === 'half-screen',
'BuilderEmbeddedCodeEditor--singleLine':
props.variant === 'single-line',
}"
>
<div ref="editorContainerEl" class="editorContainer"></div>
Expand All @@ -18,16 +20,26 @@ import "./builderEditorWorker";
import {
onMounted,
onUnmounted,
PropType,
type PropType,
toRefs,
useTemplateRef,
watch,
} from "vue";
import { syncModelWithLSP } from "./lsp/lspModelSync";
import { setModelDiagnostics, setupLSPDiagnostics } from "./lsp/lspDiagnostics";
import { useMonacopilot } from "../composables/useMonacopilot";
import { useLogger } from "@/composables/useLogger";
import { useCodeEditorSettings } from "@/composables/useCodeEditorSettings";

const rootEl = useTemplateRef("rootEl");
const editorContainerEl = useTemplateRef("editorContainerEl");
const resizeObserver = new ResizeObserver(updateDimensions);
let editor: monaco.editor.IStandaloneCodeEditor = null;
let lspSyncDisposable: monaco.IDisposable | null = null;
let diagnosticsDisposable: monaco.IDisposable | null = null;
let monacopilotCleanup: (() => void) | null = null;

const { diagnosticsEnabled, aiCompletionEnabled } = useCodeEditorSettings();

type EditorVariant = "full" | "minimal" | "half-screen" | "single-line";

Expand All @@ -44,6 +56,8 @@ const props = defineProps({
const { modelValue, disabled, language } = toRefs(props);
const emit = defineEmits(["update:modelValue"]);

const logger = useLogger();

const VARIANTS_SETTINGS: Partial<
Record<
EditorVariant,
Expand All @@ -54,6 +68,7 @@ const VARIANTS_SETTINGS: Partial<
minimap: {
enabled: false,
},
tabCompletion: "on",
},
minimal: {
minimap: {
Expand Down Expand Up @@ -94,38 +109,146 @@ watch(disabled, (isNewDisabled) => {
});

watch(modelValue, (newCode) => {
if (editor.getValue() == newCode) return;
if (!editor || editor.getValue() === newCode) return;
editor.getModel().setValue(newCode);
});

watch(language, () => {
monaco.editor.setModelLanguage(editor.getModel(), language.value);
watch(language, (newLang) => {
if (!editor) return;
const model = editor.getModel();
if (model.getLanguageId() === newLang) return;

// Dispose old LSP sync before changing language
if (lspSyncDisposable) {
lspSyncDisposable.dispose();
lspSyncDisposable = null;
}

// Change language
monaco.editor.setModelLanguage(model, newLang);

// Re-sync if new language is Python
if (newLang === "python") {
try {
lspSyncDisposable = syncModelWithLSP(model);
} catch (error) {
logger.error("Failed to re-sync model with LSP:", error);
}
}
Comment on lines +116 to +137
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Language switches only restart document sync, not the rest of the Python tooling.

This watcher reopens LSP sync, but it never mirrors the Python-only setup from onMounted(). An editor that starts in another language and later becomes Python will miss setupLSPDiagnostics() and useMonacopilot() until the user toggles those settings manually. Going the other way also leaves the previous Python markers/provider state around.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/src/builder/BuilderEmbeddedCodeEditor.vue` around lines 116 - 137, The
language watcher currently only restarts LSP sync (lspSyncDisposable via
syncModelWithLSP) but must mirror the Python-specific initialization and
teardown performed in onMounted(): when newLang becomes "python" call the same
initialization functions used on mount (e.g., setupLSPDiagnostics() and
useMonacopilot()) after re-syncing the model, and when leaving Python
dispose/disable those Python-only resources (tear down diagnostics/providers and
monacopilot) and clear any related state so prior Python markers/providers are
removed; update the watcher around the language change (referencing language,
editor, lspSyncDisposable, syncModelWithLSP, setupLSPDiagnostics,
useMonacopilot) to run the same init or cleanup paths as onMounted().

});

watch(diagnosticsEnabled, (enabled) => {
if (
!editor ||
language.value !== "python" ||
props.variant === "single-line"
) {
return;
}

const model = editor.getModel();
if (model && language.value === "python") {
diagnosticsDisposable?.dispose();
if (enabled) {
diagnosticsDisposable = setupLSPDiagnostics(monaco);
}
model.setValue(model.getValue());
}
});
Comment thread
bybash marked this conversation as resolved.

// Watch AI completion setting changes
watch(aiCompletionEnabled, async (enabled) => {
if (
!editor ||
language.value !== "python" ||
props.variant === "single-line"
)
return;

if (enabled) {
// Enable AI completion
if (!monacopilotCleanup) {
try {
monacopilotCleanup = await useMonacopilot(
monaco,
editor,
language.value,
);
} catch (error) {
logger.error("Failed to enable AI completion:", error);
}
}
} else {
// Disable AI completion
if (monacopilotCleanup) {
monacopilotCleanup();
monacopilotCleanup = null;
}
}
});
Comment thread
bybash marked this conversation as resolved.

onMounted(() => {
editor = monaco.editor.create(editorContainerEl.value, {
value: modelValue.value ?? "",
language: props.language,
onMounted(async () => {
// Create model with proper URI for LSP
const modelUri = monaco.Uri.parse(`inmemory://model/${Date.now()}.py`);
const model = monaco.editor.createModel(
modelValue.value ?? "",
language.value || "python",
language.value === "python" ? modelUri : undefined,
);

editor = monaco.editor.create(editorContainerEl.value as HTMLElement, {
model: model,
readOnly: props.disabled,
fixedOverflowWidgets: true,
quickSuggestions: {
other: true,
comments: true,
strings: true,
},
...VARIANTS_SETTINGS[props.variant],
});
editor.getModel().onDidChangeContent(() => {

model.onDidChangeContent(() => {
const newCode = editor.getValue();
emit("update:modelValue", newCode);
});
resizeObserver.observe(rootEl.value);

resizeObserver.observe(rootEl.value as Element);

// Manually sync model with LSP for Python language
// This is required because we're in a browser (no filesystem)
if (language.value === "python" && props.variant !== "single-line") {
try {
lspSyncDisposable = syncModelWithLSP(model);
} catch (error) {
logger.error("Failed to sync model with LSP:", error);
}

// Register AI-powered code completions (if AI completion enabled)
if (aiCompletionEnabled.value) {
try {
monacopilotCleanup = await useMonacopilot(
monaco,
editor,
language.value,
);
} catch (error) {
logger.error("Failed to initialize monacopilot:", error);
}
}

diagnosticsDisposable?.dispose();
if (diagnosticsEnabled.value) {
diagnosticsDisposable = setupLSPDiagnostics(monaco);
}
Comment on lines +218 to +243
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The first Python editor can silently miss LSP wiring during startup.

src/ui/src/main.ts mounts the app before awaiting setupLSP(). If this block runs while the client is still coming up, syncModelWithLSP() and setupLSPDiagnostics() both fall back to no-op disposables, and this component never retries after the client registers. The initial editor instance then stays without LSP-backed diagnostics/completions until something else remounts or reinitializes it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/src/builder/BuilderEmbeddedCodeEditor.vue` around lines 218 - 243, The
startup race causes syncModelWithLSP and setupLSPDiagnostics to be no-ops if the
LSP client isn't ready; change the logic in BuilderEmbeddedCodeEditor to defer/
retry wiring when LSP is unavailable: detect the LSP-ready state (or await a
provided promise/event from your LSP bootstrap) and only call
syncModelWithLSP(model) and setupLSPDiagnostics(monaco) after the client is
ready, or subscribe to an LSP-ready event and perform the wiring there (ensuring
you dispose any previous lspSyncDisposable/diagnosticsDisposable before
reassigning); apply the same deferred initialization for useMonacopilot so
completions are registered once LSP is up.

}

// when in modal, focus the editor and set the cursor to the last line
if (props.variant === "half-screen") {
editor.focus();
editor.setPosition({
lineNumber: editor.getModel().getLineCount(),
column: editor
.getModel()
.getLineLastNonWhitespaceColumn(
editor.getModel().getLineCount(),
),
lineNumber: model.getLineCount(),
column: model.getLineLastNonWhitespaceColumn(model.getLineCount()),
});
}
});
Expand All @@ -135,7 +258,32 @@ function updateDimensions() {
}

onUnmounted(() => {
editor.dispose();
// Clean up LSP sync
if (lspSyncDisposable) {
lspSyncDisposable.dispose();
lspSyncDisposable = null;
}

const model = editor?.getModel();

// Clear diagnostics before disposing model
if (model && language.value === "python") {
setModelDiagnostics(monaco, model, []);
}

if (editor) {
editor.dispose();
}
if (model) {
model.dispose();
}
if (monacopilotCleanup) {
monacopilotCleanup();
}
if (diagnosticsDisposable) {
diagnosticsDisposable.dispose();
diagnosticsDisposable = null;
}
resizeObserver.disconnect();
});
</script>
Expand Down
Loading