langflower 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +20 -15
  2. package/docs/public/README.md +15 -0
  3. package/docs/public/configuration.md +56 -0
  4. package/docs/public/extending.md +58 -0
  5. package/docs/public/getting-started.md +61 -0
  6. package/docs/public/how-it-works.md +39 -0
  7. package/docs/public/product.md +43 -0
  8. package/docs/public/using-the-editor.md +53 -0
  9. package/docs/public/workflows.md +49 -0
  10. package/package.json +3 -2
  11. package/ui-dist/chunk-6DQ2XSRW.js +4 -0
  12. package/ui-dist/{chunk-2AICAC67.js → chunk-Y6VMBLSJ.js} +1 -1
  13. package/ui-dist/index.html +2 -2
  14. package/ui-dist/main-RCFTD5AZ.js +311 -0
  15. package/ui-dist/styles-EOXODOI7.css +1 -0
  16. package/vendor/common-nodes/dist/ai/critique/node.d.ts +141 -23
  17. package/vendor/common-nodes/dist/ai/critique/node.js +33 -31
  18. package/vendor/common-nodes/dist/ai/fake-llm/node.d.ts +6 -6
  19. package/vendor/common-nodes/dist/ai/llm-compaction-ui-schema.d.ts +1 -1
  20. package/vendor/common-nodes/dist/ai/llm-compaction-ui-schema.js +2 -1
  21. package/vendor/common-nodes/dist/ai/llm-panel-ui-schema.d.ts +7 -21
  22. package/vendor/common-nodes/dist/ai/llm-panel-ui-schema.js +8 -4
  23. package/vendor/common-nodes/dist/ai/llm-session-shell.d.ts +2 -0
  24. package/vendor/common-nodes/dist/ai/llm-session-shell.js +3 -3
  25. package/vendor/common-nodes/dist/ai/normalize-max-iterations.d.ts +4 -3
  26. package/vendor/common-nodes/dist/ai/normalize-max-iterations.js +4 -3
  27. package/vendor/common-nodes/dist/ai/openai/normalize-compaction-params.d.ts +2 -2
  28. package/vendor/common-nodes/dist/ai/openai/normalize-compaction-params.js +1 -1
  29. package/vendor/common-nodes/dist/ai/openai-llm/node.d.ts +9 -9
  30. package/vendor/common-nodes/dist/ai/resolve-chat-provider-model.d.ts +10 -0
  31. package/vendor/common-nodes/dist/ai/resolve-chat-provider-model.js +8 -0
  32. package/vendor/common-nodes/dist/ai/review/node.d.ts +138 -15
  33. package/vendor/common-nodes/dist/ai/review/node.js +29 -22
  34. package/vendor/common-nodes/dist/ai/run-host-services.d.ts +8 -0
  35. package/vendor/common-nodes/dist/ai/sub-agent/node.d.ts +9 -9
  36. package/vendor/common-nodes/dist/ai/sub-agent/node.js +4 -3
  37. package/vendor/common-nodes/dist/hitl/chat-input/node.js +1 -0
  38. package/vendor/common-nodes/dist/hitl/review-gate/node.js +5 -0
  39. package/vendor/node-sdk/dist/node-factory/define-llm-node/default-llm-ports.js +2 -0
  40. package/vendor/node-sdk/dist/node-factory/define-reactive-node/define-reactive-node.d.ts +1 -1
  41. package/vendor/node-sdk/dist/node-factory/define-reactive-node/io-helpers.d.ts +5 -12
  42. package/vendor/node-sdk/dist/node-factory/define-reactive-node/port-meta.d.ts +13 -0
  43. package/vendor/runtime/dist/runtime-runner.js +7 -4
  44. package/vendor/runtime/dist/runtime.d.ts +1 -1
  45. package/vendor/runtime/dist/types.d.ts +12 -0
  46. package/vendor/server/dist/bridge/attach-langflower-bridge.d.ts +4 -1
  47. package/vendor/server/dist/bridge/attach-langflower-bridge.js +16 -5
  48. package/vendor/server/dist/bridge/bridge-event-log.d.ts +12 -0
  49. package/vendor/server/dist/bridge/bridge-event-log.js +199 -0
  50. package/vendor/server/dist/bridge/build-execution-context.d.ts +1 -1
  51. package/vendor/server/dist/bridge/build-execution-context.js +3 -0
  52. package/vendor/server/dist/bridge/emit-bootstrap.d.ts +4 -2
  53. package/vendor/server/dist/bridge/emit-bootstrap.js +6 -2
  54. package/vendor/server/dist/bridge/settings-draft-controller.d.ts +14 -0
  55. package/vendor/server/dist/bridge/settings-draft-controller.js +178 -0
  56. package/vendor/server/dist/bridge/wire-config-handlers.d.ts +8 -1
  57. package/vendor/server/dist/bridge/wire-config-handlers.js +44 -16
  58. package/vendor/server/dist/bridge/wire-editor-handlers.d.ts +8 -1
  59. package/vendor/server/dist/bridge/wire-editor-handlers.js +40 -1
  60. package/vendor/server/dist/bridge/wire-runner-handlers.js +3 -1
  61. package/vendor/server/dist/config/langflower-config.service.d.ts +2 -0
  62. package/vendor/server/dist/config/langflower-config.service.js +24 -1
  63. package/vendor/server/dist/config/resolve-draft-provider-credentials.d.ts +18 -0
  64. package/vendor/server/dist/config/resolve-draft-provider-credentials.js +60 -0
  65. package/vendor/server/dist/create-server.js +5 -3
  66. package/vendor/server/dist/session/build-session-bootstrap.d.ts +3 -1
  67. package/vendor/server/dist/session/build-session-bootstrap.js +9 -0
  68. package/vendor/server/dist/session/langflower-session.d.ts +7 -2
  69. package/vendor/server/dist/session/langflower-session.js +9 -1
  70. package/vendor/server/dist/session/settings-draft-session.d.ts +24 -0
  71. package/vendor/server/dist/session/settings-draft-session.js +82 -0
  72. package/vendor/server/skeleton/schemas/langflower-config.schema.json +4 -0
  73. package/vendor/server/skeleton/skills/langflower-helper/SKILL.md +20 -8
  74. package/vendor/server/skeleton/skills/langflower-helper/architecture.md +5 -0
  75. package/vendor/server/skeleton/skills/langflower-helper/layout.md +18 -16
  76. package/vendor/server/skeleton/workflows/simple-coder.json +249 -19
  77. package/vendor/server/skeleton/workflows/starter.json +31 -20
  78. package/vendor/shared/dist/langflower-bus-config.d.ts +50 -10
  79. package/vendor/shared/dist/langflower-bus-config.js +52 -11
  80. package/vendor/shared/dist/langflower-config/merge-langflower-config-layers.test.js +4 -0
  81. package/vendor/shared/dist/langflower-config/parse-default-chat-model.d.ts +14 -0
  82. package/vendor/shared/dist/langflower-config/parse-default-chat-model.js +38 -0
  83. package/vendor/shared/dist/langflower-config/parse-default-chat-model.test.d.ts +1 -0
  84. package/vendor/shared/dist/langflower-config/parse-default-chat-model.test.js +41 -0
  85. package/vendor/shared/dist/langflower-config/resolve-server-logs-enabled.d.ts +6 -0
  86. package/vendor/shared/dist/langflower-config/resolve-server-logs-enabled.js +5 -0
  87. package/vendor/shared/dist/langflower-config/resolve-server-logs-enabled.test.d.ts +1 -0
  88. package/vendor/shared/dist/langflower-config/resolve-server-logs-enabled.test.js +11 -0
  89. package/vendor/shared/dist/langflower-config/settings-draft.d.ts +44 -0
  90. package/vendor/shared/dist/langflower-config/settings-draft.js +143 -0
  91. package/vendor/shared/dist/langflower.d.ts +7 -2
  92. package/vendor/shared/dist/langflower.js +3 -0
  93. package/vendor/shared/dist/types/langflower-bootstrap.d.ts +6 -1
  94. package/vendor/shared/dist/types/langflower-config.d.ts +65 -0
  95. package/vendor/shared/dist/types/langflower-editor.d.ts +19 -0
  96. package/ui-dist/chunk-6ZABW4JL.js +0 -4
  97. package/ui-dist/main-XI7IOZKZ.js +0 -311
  98. package/ui-dist/styles-TQGRAC7Q.css +0 -1
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Session Settings draft: seed, patch, discard, save commit, connection probe.
3
+ */
4
+ import { listProviderModels as listOpenAiProviderModels } from '@langflower/common-nodes/ai/openai/list-provider-models';
5
+ import { draftToSavePayload, mergeLangflowerConfigLayers, } from '@langflower/shared/langflower.js';
6
+ import { resolveDraftProviderCredentials } from '../config/resolve-draft-provider-credentials.js';
7
+ import { applyDraftPatch, buildDraftSnapshot, providerIndexesWithBaseUrl, seedScopeDraft, setConnectionStatus, } from '../session/settings-draft-session.js';
8
+ import { bridgeEmit, clientEmit } from './bridge-outbound.js';
9
+ import { buildLangflowerConfigSnapshot } from './build-langflower-config-snapshot.js';
10
+ const isScope = (value) => value === 'project' || value === 'global';
11
+ const layerForScope = (layers, scope) => (scope === 'project' ? layers.project : layers.global);
12
+ export const createSettingsDraftController = (bridge, context, session) => {
13
+ const probeGeneration = new Map();
14
+ const getState = (scope) => scope === 'project'
15
+ ? session.settingsDraft.project
16
+ : session.settingsDraft.global;
17
+ const setState = (scope, state) => {
18
+ if (scope === 'project') {
19
+ session.settingsDraft.project = state;
20
+ }
21
+ else {
22
+ session.settingsDraft.global = state;
23
+ }
24
+ };
25
+ /** Emit current session draft — never re-enters ensureSeeded/probe. */
26
+ const emitCurrent = (scope) => {
27
+ const state = getState(scope);
28
+ if (state === undefined) {
29
+ return;
30
+ }
31
+ bridgeEmit(bridge, 'langflower.config.draft.snapshot', buildDraftSnapshot(scope, state));
32
+ };
33
+ const runProbe = async (scope, index) => {
34
+ const key = `${scope}:${index}`;
35
+ const generation = (probeGeneration.get(key) ?? 0) + 1;
36
+ probeGeneration.set(key, generation);
37
+ const state = getState(scope);
38
+ const row = state?.draft.providers[index];
39
+ if (state === undefined || row === undefined) {
40
+ return;
41
+ }
42
+ const baseURL = row.baseURL.trim();
43
+ if (baseURL.length === 0) {
44
+ setState(scope, setConnectionStatus(state, index, { state: 'idle' }));
45
+ emitCurrent(scope);
46
+ return;
47
+ }
48
+ setState(scope, setConnectionStatus(state, index, { state: 'checking' }));
49
+ emitCurrent(scope);
50
+ const layers = await context.langflowerConfigService.readLayers();
51
+ const savedLayer = layerForScope(layers, scope);
52
+ const resolved = resolveDraftProviderCredentials(row, savedLayer);
53
+ let status;
54
+ if (!resolved.ok) {
55
+ status = { state: 'error', message: resolved.message };
56
+ }
57
+ else {
58
+ const listed = await listOpenAiProviderModels(resolved.credentials);
59
+ status =
60
+ listed.error !== undefined
61
+ ? { state: 'error', message: listed.error }
62
+ : { state: 'ok', modelCount: listed.models.length };
63
+ }
64
+ if (probeGeneration.get(key) !== generation) {
65
+ return;
66
+ }
67
+ const latest = getState(scope);
68
+ if (latest === undefined ||
69
+ latest.draft.providers[index] === undefined) {
70
+ return;
71
+ }
72
+ setState(scope, setConnectionStatus(latest, index, status));
73
+ emitCurrent(scope);
74
+ };
75
+ const probeProvidersWithUrl = (scope, draft) => {
76
+ for (const index of providerIndexesWithBaseUrl(draft)) {
77
+ void runProbe(scope, index);
78
+ }
79
+ };
80
+ const ensureSeeded = async (scope) => {
81
+ if (getState(scope) !== undefined) {
82
+ return;
83
+ }
84
+ const layers = await context.langflowerConfigService.readLayers();
85
+ const seeded = seedScopeDraft(layerForScope(layers, scope));
86
+ setState(scope, seeded);
87
+ // Seed leaves URL rows as `checking`; kick probes so launch shows
88
+ // Connected/error without requiring an edit first.
89
+ probeProvidersWithUrl(scope, seeded.draft);
90
+ };
91
+ const snapshotFor = async (scope) => {
92
+ await ensureSeeded(scope);
93
+ const state = getState(scope);
94
+ if (state === undefined) {
95
+ return null;
96
+ }
97
+ return buildDraftSnapshot(scope, state);
98
+ };
99
+ const broadcast = async (scope) => {
100
+ await ensureSeeded(scope);
101
+ emitCurrent(scope);
102
+ };
103
+ const pushToClient = async (client, scope) => {
104
+ const snapshot = await snapshotFor(scope);
105
+ if (snapshot === null) {
106
+ return;
107
+ }
108
+ clientEmit(client, 'langflower.config.draft.snapshot', snapshot);
109
+ };
110
+ const patch = async (scope, draft) => {
111
+ await ensureSeeded(scope);
112
+ const previous = getState(scope);
113
+ if (previous === undefined) {
114
+ return;
115
+ }
116
+ const { next, probeIndexes } = applyDraftPatch(previous, draft);
117
+ setState(scope, next);
118
+ emitCurrent(scope);
119
+ for (const index of probeIndexes) {
120
+ void runProbe(scope, index);
121
+ }
122
+ };
123
+ const discard = async (scope) => {
124
+ const layers = await context.langflowerConfigService.readLayers();
125
+ const seeded = seedScopeDraft(layerForScope(layers, scope));
126
+ setState(scope, seeded);
127
+ emitCurrent(scope);
128
+ probeProvidersWithUrl(scope, seeded.draft);
129
+ };
130
+ const commitSave = async (scope, fallback) => {
131
+ await ensureSeeded(scope);
132
+ const state = getState(scope);
133
+ // Prefer an explicit Save payload (provider/model/serverLogs) so a
134
+ // trailing draft.patch cannot race past commit. Otherwise persist the
135
+ // session draft (`{ scope }` only).
136
+ const payload = fallback !== undefined &&
137
+ (fallback.provider !== undefined ||
138
+ fallback.model !== undefined ||
139
+ 'serverLogs' in fallback)
140
+ ? fallback
141
+ : state !== undefined
142
+ ? draftToSavePayload(scope, state.draft)
143
+ : fallback;
144
+ if (payload === undefined || !isScope(payload.scope)) {
145
+ return null;
146
+ }
147
+ const layers = await context.langflowerConfigService.writeSettings({
148
+ scope: payload.scope,
149
+ ...(payload.model !== undefined ? { model: payload.model } : {}),
150
+ ...(payload.provider !== undefined
151
+ ? { provider: payload.provider }
152
+ : {}),
153
+ ...(payload.providerApiKeys !== undefined
154
+ ? { providerApiKeys: payload.providerApiKeys }
155
+ : {}),
156
+ ...('serverLogs' in payload
157
+ ? { serverLogs: payload.serverLogs }
158
+ : {}),
159
+ });
160
+ const layer = layerForScope(layers, scope);
161
+ const seeded = seedScopeDraft(layer);
162
+ setState(scope, seeded);
163
+ const snapshot = await buildLangflowerConfigSnapshot(context);
164
+ bridgeEmit(bridge, 'langflower.config.snapshot', snapshot);
165
+ emitCurrent(scope);
166
+ probeProvidersWithUrl(scope, seeded.draft);
167
+ return mergeLangflowerConfigLayers(layers.global, layers.project);
168
+ };
169
+ return {
170
+ ensureSeeded,
171
+ snapshotFor,
172
+ broadcast,
173
+ pushToClient,
174
+ patch,
175
+ discard,
176
+ commitSave,
177
+ };
178
+ };
@@ -1,4 +1,11 @@
1
1
  import type { Subscription } from 'rxjs';
2
+ import type { LangflowerConfig } from '@langflower/shared/langflower.js';
2
3
  import type { ServerContext } from '../server-context.js';
4
+ import type { LangflowerSession } from '../session/langflower-session.js';
3
5
  import type { LangflowerBridge } from './langflower-bridge.types.js';
4
- export declare const wireConfigHandlers: (bridge: LangflowerBridge, context: ServerContext) => Subscription;
6
+ import type { SettingsDraftController } from './settings-draft-controller.js';
7
+ export type WireConfigHandlersOptions = {
8
+ readonly onEffectiveConfig?: (config: LangflowerConfig) => void;
9
+ };
10
+ export declare const wireConfigHandlers: (bridge: LangflowerBridge, context: ServerContext, _session: LangflowerSession, draftController: SettingsDraftController, options?: WireConfigHandlersOptions) => Subscription;
11
+ export declare const applyServerLogsGate: (setEnabled: (enabled: boolean) => void, config: LangflowerConfig) => void;
@@ -1,21 +1,49 @@
1
- import { bridgeEmit } from './bridge-outbound.js';
2
- import { buildLangflowerConfigSnapshot } from './build-langflower-config-snapshot.js';
1
+ import { resolveServerLogsEnabled } from '@langflower/shared/langflower.js';
3
2
  import { isInboundEvent } from './inbound-guards.js';
4
3
  import { broadcastModelsCatalog } from './push-models-catalog.js';
5
4
  const isConfigScope = (value) => value === 'project' || value === 'global';
6
- export const wireConfigHandlers = (bridge, context) => bridge['langflower.config.save.requested'].subscribe(async (raw) => {
7
- if (!isInboundEvent(raw) ||
8
- !isConfigScope(raw.payload.scope)) {
9
- return;
5
+ const isSettingsDraft = (value) => {
6
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
7
+ return false;
10
8
  }
11
- const { scope, model, provider, providerApiKeys } = raw.payload;
12
- await context.langflowerConfigService.writeSettings({
13
- scope,
14
- ...(model !== undefined ? { model } : {}),
15
- ...(provider !== undefined ? { provider } : {}),
16
- ...(providerApiKeys !== undefined ? { providerApiKeys } : {}),
9
+ const draft = value;
10
+ return (typeof draft['defaultProviderId'] === 'string' &&
11
+ typeof draft['defaultModelId'] === 'string' &&
12
+ Array.isArray(draft['providers']) &&
13
+ (draft['serverLogs'] === 'off' ||
14
+ draft['serverLogs'] === 'default' ||
15
+ draft['serverLogs'] === 'on'));
16
+ };
17
+ export const wireConfigHandlers = (bridge, context, _session, draftController, options = {}) => {
18
+ const subscription = bridge['langflower.config.save.requested'].subscribe(async (raw) => {
19
+ if (!isInboundEvent(raw) ||
20
+ !isConfigScope(raw.payload.scope)) {
21
+ return;
22
+ }
23
+ const effective = await draftController.commitSave(raw.payload.scope, raw.payload);
24
+ if (effective === null) {
25
+ return;
26
+ }
27
+ options.onEffectiveConfig?.(effective);
28
+ await broadcastModelsCatalog(bridge, context);
17
29
  });
18
- const snapshot = await buildLangflowerConfigSnapshot(context);
19
- bridgeEmit(bridge, 'langflower.config.snapshot', snapshot);
20
- await broadcastModelsCatalog(bridge, context);
21
- });
30
+ subscription.add(bridge['langflower.config.draft.patch.requested'].subscribe(async (raw) => {
31
+ if (!isInboundEvent(raw) ||
32
+ !isConfigScope(raw.payload.scope) ||
33
+ !isSettingsDraft(raw.payload.draft)) {
34
+ return;
35
+ }
36
+ await draftController.patch(raw.payload.scope, raw.payload.draft);
37
+ }));
38
+ subscription.add(bridge['langflower.config.draft.discard.requested'].subscribe(async (raw) => {
39
+ if (!isInboundEvent(raw) ||
40
+ !isConfigScope(raw.payload.scope)) {
41
+ return;
42
+ }
43
+ await draftController.discard(raw.payload.scope);
44
+ }));
45
+ return subscription;
46
+ };
47
+ export const applyServerLogsGate = (setEnabled, config) => {
48
+ setEnabled(resolveServerLogsEnabled(config));
49
+ };
@@ -1,5 +1,12 @@
1
+ import { type EditorSettingsRequestedPayload, type EditorSettingsSnapshotPayload } from '@langflower/shared/langflower.js';
1
2
  import { Subscription } from 'rxjs';
2
3
  import type { ServerContext } from '../server-context.js';
3
4
  import type { LangflowerSession } from '../session/langflower-session.js';
4
5
  import type { LangflowerBridge } from './langflower-bridge.types.js';
5
- export declare const wireEditorHandlers: (bridge: LangflowerBridge, context: ServerContext, session: LangflowerSession) => Subscription;
6
+ import type { SettingsDraftController } from './settings-draft-controller.js';
7
+ export declare const wireEditorHandlers: (bridge: LangflowerBridge, context: ServerContext, session: LangflowerSession, draftController?: SettingsDraftController) => Subscription;
8
+ /**
9
+ * Apply a Settings aside intent. Returns `null` when the payload is invalid
10
+ * (`open: true` without a valid `scope`).
11
+ */
12
+ export declare const applySettingsRequested: (current: EditorSettingsSnapshotPayload, payload: EditorSettingsRequestedPayload) => EditorSettingsSnapshotPayload | null;
@@ -19,7 +19,7 @@ const isValidCanvasViewport = (value) => {
19
19
  Number.isFinite(viewport.scale) &&
20
20
  viewport.scale > 0);
21
21
  };
22
- export const wireEditorHandlers = (bridge, context, session) => {
22
+ export const wireEditorHandlers = (bridge, context, session, draftController) => {
23
23
  const subscription = new Subscription();
24
24
  subscription.add(bridge['editor.addNode.requested'].subscribe((raw) => {
25
25
  if (!isInboundEvent(raw)) {
@@ -182,6 +182,23 @@ export const wireEditorHandlers = (bridge, context, session) => {
182
182
  bridgeEmit(bridge, 'editor.dividers.snapshot', clamped);
183
183
  void context.langflowerConfigService.setDividerPositions(clamped);
184
184
  }));
185
+ subscription.add(bridge['editor.settings.requested'].subscribe((raw) => {
186
+ if (!isInboundEvent(raw)) {
187
+ return;
188
+ }
189
+ if (findClientById(bridge, raw.clientId) === undefined) {
190
+ return;
191
+ }
192
+ const next = applySettingsRequested(session.settings, raw.payload);
193
+ if (next === null) {
194
+ return;
195
+ }
196
+ session.settings = next;
197
+ bridgeEmit(bridge, 'editor.settings.snapshot', next);
198
+ if (next.open && draftController !== undefined) {
199
+ void draftController.broadcast(next.scope);
200
+ }
201
+ }));
185
202
  subscription.add(bridge['editor.selectNode.requested'].subscribe((raw) => {
186
203
  if (!isInboundEvent(raw)) {
187
204
  return;
@@ -194,8 +211,30 @@ export const wireEditorHandlers = (bridge, context, session) => {
194
211
  !session.activeWorkflow?.graph.nodes.some((node) => node.id === nodeId)) {
195
212
  return;
196
213
  }
214
+ if (nodeId !== null && session.settings.open) {
215
+ const closed = {
216
+ open: false,
217
+ scope: session.settings.scope,
218
+ };
219
+ session.settings = closed;
220
+ bridgeEmit(bridge, 'editor.settings.snapshot', closed);
221
+ }
197
222
  session.selectedNodeId = nodeId;
198
223
  bridgeEmit(bridge, 'editor.nodeSelected', buildSelectedNodePayload(session, context.resolveDefinition));
199
224
  }));
200
225
  return subscription;
201
226
  };
227
+ const isConfigScope = (value) => value === 'project' || value === 'global';
228
+ /**
229
+ * Apply a Settings aside intent. Returns `null` when the payload is invalid
230
+ * (`open: true` without a valid `scope`).
231
+ */
232
+ export const applySettingsRequested = (current, payload) => {
233
+ if (payload.open) {
234
+ if (!isConfigScope(payload.scope)) {
235
+ return null;
236
+ }
237
+ return { open: true, scope: payload.scope };
238
+ }
239
+ return { open: false, scope: current.scope };
240
+ };
@@ -273,7 +273,9 @@ export const wireRunnerHandlers = (bridge, context, session, checkpoints) => {
273
273
  if (connected === undefined) {
274
274
  return;
275
275
  }
276
- session.permissionAsks.reply(raw.payload);
276
+ if (session.permissionAsks.reply(raw.payload)) {
277
+ bridgeEmit(bridge, 'runner.permission.accepted', raw.payload);
278
+ }
277
279
  }));
278
280
  subscription.add(bridge['runner.executionFeed.clear.requested'].subscribe((raw) => {
279
281
  if (!isInboundEvent(raw)) {
@@ -8,6 +8,8 @@ export type LangflowerConfigSettingsWrite = {
8
8
  readonly model?: string;
9
9
  readonly provider?: Readonly<Record<string, LangflowerProviderConfig>>;
10
10
  readonly providerApiKeys?: Readonly<Record<string, string>>;
11
+ /** `null` clears the scope key (Settings Default). */
12
+ readonly serverLogs?: boolean | null;
11
13
  };
12
14
  export declare class LangflowerConfigService {
13
15
  private readonly projectDir;
@@ -147,6 +147,9 @@ function parseLangflowerConfig(raw) {
147
147
  ? { currentWorkflowId: raw.currentWorkflowId }
148
148
  : {}),
149
149
  ...(typeof raw.model === 'string' ? { model: raw.model } : {}),
150
+ ...(typeof raw.serverLogs === 'boolean'
151
+ ? { serverLogs: raw.serverLogs }
152
+ : {}),
150
153
  ...(providers !== undefined ? { provider: providers } : {}),
151
154
  ...(tools !== undefined ? { tools } : {}),
152
155
  ...(permission !== undefined ? { permission } : {}),
@@ -195,7 +198,20 @@ function mergeLangflowerConfig(existing, patch) {
195
198
  }
196
199
  }
197
200
  if (patch.model !== undefined) {
198
- merged.model = patch.model;
201
+ if (patch.model.trim().length === 0) {
202
+ delete merged.model;
203
+ }
204
+ else {
205
+ merged.model = patch.model;
206
+ }
207
+ }
208
+ if ('serverLogs' in patch) {
209
+ if (patch.serverLogs === undefined) {
210
+ delete merged.serverLogs;
211
+ }
212
+ else {
213
+ merged.serverLogs = patch.serverLogs;
214
+ }
199
215
  }
200
216
  if (patch.provider !== undefined) {
201
217
  merged.provider = patch.provider;
@@ -315,6 +331,13 @@ export class LangflowerConfigService {
315
331
  const patch = {
316
332
  ...(input.model !== undefined ? { model: input.model } : {}),
317
333
  ...(provider !== undefined ? { provider } : {}),
334
+ ...('serverLogs' in input
335
+ ? {
336
+ serverLogs: input.serverLogs === null
337
+ ? undefined
338
+ : input.serverLogs,
339
+ }
340
+ : {}),
318
341
  };
319
342
  const merged = mergeLangflowerConfig(existingRaw, patch);
320
343
  await this.writeRawAt(filePath, merged);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Resolve OpenAI-compatible credentials for a Settings draft row probe.
3
+ * Prefer pending draft apiKey; else fall back to the saved layer provider.
4
+ */
5
+ import type { LangflowerConfig, ProviderDraft } from '@langflower/shared/langflower.js';
6
+ type ResolvedCredentials = {
7
+ readonly apiKey?: string;
8
+ readonly baseURL?: string;
9
+ };
10
+ export type ResolveDraftProviderCredentialsResult = {
11
+ readonly ok: true;
12
+ readonly credentials: ResolvedCredentials;
13
+ } | {
14
+ readonly ok: false;
15
+ readonly message: string;
16
+ };
17
+ export declare const resolveDraftProviderCredentials: (row: ProviderDraft, savedLayer: LangflowerConfig) => ResolveDraftProviderCredentialsResult;
18
+ export {};
@@ -0,0 +1,60 @@
1
+ import { resolveProviderCredentials } from './resolve-provider-credentials.js';
2
+ const ENV_REF_PATTERN = /^\{env:([A-Za-z_][A-Za-z0-9_]*)\}$/;
3
+ const resolveEnvRef = (value, context) => {
4
+ const match = ENV_REF_PATTERN.exec(value);
5
+ if (match === null) {
6
+ return { ok: true, value };
7
+ }
8
+ const varName = match[1];
9
+ const resolved = process.env[varName];
10
+ if (resolved === undefined || resolved === '') {
11
+ return {
12
+ ok: false,
13
+ message: `Environment variable ${varName} is not set (${context})`,
14
+ };
15
+ }
16
+ return { ok: true, value: resolved };
17
+ };
18
+ export const resolveDraftProviderCredentials = (row, savedLayer) => {
19
+ const baseURL = row.baseURL.trim();
20
+ if (baseURL.length === 0) {
21
+ return {
22
+ ok: false,
23
+ message: 'Base URL is empty',
24
+ };
25
+ }
26
+ const context = `draft provider ${row.id.trim() || '(new)'}`;
27
+ const pendingKey = row.apiKey.trim();
28
+ if (pendingKey.length > 0) {
29
+ const resolved = resolveEnvRef(pendingKey, context);
30
+ if (!resolved.ok) {
31
+ return resolved;
32
+ }
33
+ return {
34
+ ok: true,
35
+ credentials: {
36
+ apiKey: resolved.value,
37
+ baseURL,
38
+ },
39
+ };
40
+ }
41
+ const providerId = row.id.trim();
42
+ if (providerId.length > 0 &&
43
+ savedLayer.provider?.[providerId] !== undefined) {
44
+ const fromSaved = resolveProviderCredentials(savedLayer, providerId);
45
+ if (!fromSaved.ok) {
46
+ return fromSaved;
47
+ }
48
+ return {
49
+ ok: true,
50
+ credentials: {
51
+ ...fromSaved.credentials,
52
+ baseURL,
53
+ },
54
+ };
55
+ }
56
+ return {
57
+ ok: true,
58
+ credentials: { baseURL },
59
+ };
60
+ };
@@ -1,5 +1,5 @@
1
1
  import { hasCustomNodePacks } from '@langflower/compiler/compile-project-nodes';
2
- import { langflowerWsConfig, } from '@langflower/shared/langflower.js';
2
+ import { langflowerWsConfig, resolveServerLogsEnabled, } from '@langflower/shared/langflower.js';
3
3
  import { createServer as createWsBridge } from '@langflower/websocket-bridge/create-server';
4
4
  import express from 'express';
5
5
  import http from 'node:http';
@@ -33,23 +33,25 @@ export async function createServer(options) {
33
33
  });
34
34
  }
35
35
  const toolConfig = await context.configService.read();
36
+ const langflowerConfig = await context.langflowerConfigService.read();
36
37
  const port = options.port ?? toolConfig.port;
37
38
  const httpServer = http.createServer(app);
38
39
  const bridge = createWsBridge(langflowerWsConfig, { httpServer });
39
40
  const detachBridge = attachLangflowerBridge(bridge, context, {
41
+ serverLogsEnabled: resolveServerLogsEnabled(langflowerConfig),
40
42
  ...(options.onRunSettled !== undefined
41
43
  ? { onRunSettled: options.onRunSettled }
42
44
  : {}),
43
45
  });
44
46
  httpServer.on('close', () => {
45
- detachBridge();
47
+ void detachBridge();
46
48
  bridge.close();
47
49
  });
48
50
  try {
49
51
  await listenHttpServer(httpServer, port, '127.0.0.1');
50
52
  }
51
53
  catch (error) {
52
- detachBridge();
54
+ await detachBridge();
53
55
  bridge.close();
54
56
  httpServer.close();
55
57
  throw error;
@@ -1,5 +1,7 @@
1
- import type { SessionStateSnapshotPayload } from '@langflower/shared/langflower.js';
1
+ import type { EditorSettingsSnapshotPayload, SessionStateSnapshotPayload } from '@langflower/shared/langflower.js';
2
2
  import type { LangflowerConfigService } from '../config/langflower-config.service.js';
3
3
  import type { ResolveNodeDefinition } from '../workflow/workflow-document.js';
4
4
  import { LangflowerSession } from './langflower-session.js';
5
+ /** Empty effective providers → open Global Settings for bootstrap onboarding. */
6
+ export declare const settingsAsideForEffectiveConfig: (provider: Readonly<Record<string, unknown>> | undefined, current: EditorSettingsSnapshotPayload) => EditorSettingsSnapshotPayload;
5
7
  export declare function buildSessionBootstrap(session: LangflowerSession, langflowerConfigService: LangflowerConfigService, resolveDefinition: ResolveNodeDefinition, projectDir: string): Promise<SessionStateSnapshotPayload>;
@@ -2,12 +2,21 @@ import { redactLangflowerConfigForBridge } from '../config/redact-langflower-con
2
2
  import { withSkillsCatalog } from '../skills/with-skills-catalog.js';
3
3
  import { buildSelectedNodePayload } from './build-selected-node-payload.js';
4
4
  import { LangflowerSession } from './langflower-session.js';
5
+ /** Empty effective providers → open Global Settings for bootstrap onboarding. */
6
+ export const settingsAsideForEffectiveConfig = (provider, current) => {
7
+ if (Object.keys(provider ?? {}).length === 0) {
8
+ return { open: true, scope: 'global' };
9
+ }
10
+ return current;
11
+ };
5
12
  export async function buildSessionBootstrap(session, langflowerConfigService, resolveDefinition, projectDir) {
6
13
  const langflowerConfig = redactLangflowerConfigForBridge(await withSkillsCatalog(projectDir, await langflowerConfigService.read()));
14
+ session.settings = settingsAsideForEffectiveConfig(langflowerConfig.provider, session.settings);
7
15
  return {
8
16
  version: LangflowerSession.snapshotVersion,
9
17
  langflowerConfig,
10
18
  dividerPositions: session.dividerPositions,
11
19
  selectedNode: buildSelectedNodePayload(session, resolveDefinition).node,
20
+ settings: session.settings,
12
21
  };
13
22
  }
@@ -1,7 +1,8 @@
1
1
  import type { RunId, RuntimeRunnerStatus } from '@langflower/runtime';
2
2
  import { RuntimeFacade } from '@langflower/runtime';
3
- import type { DividerPositions, ExecutionFeedSnapshotPayload, WorkflowCurrentStatus, WorkflowLoadedPayload } from '@langflower/shared/langflower.js';
3
+ import type { DividerPositions, EditorSettingsSnapshotPayload, ExecutionFeedSnapshotPayload, WorkflowCurrentStatus, WorkflowLoadedPayload } from '@langflower/shared/langflower.js';
4
4
  import { PendingPermissionAsks } from '../harness/pending-permission-asks.js';
5
+ import { type SettingsDraftStore } from './settings-draft-session.js';
5
6
  export declare class LangflowerSession {
6
7
  readonly runtime: RuntimeFacade;
7
8
  /** Feed permission.ask pause/resume for the internal tool loop. */
@@ -15,6 +16,10 @@ export declare class LangflowerSession {
15
16
  dividerPositions: DividerPositions;
16
17
  /** In-memory canvas selection, shared across tabs. `null` = nothing selected. */
17
18
  selectedNodeId: string | null;
19
+ /** Settings aside chrome — session memory only (not persisted to disk). */
20
+ settings: EditorSettingsSnapshotPayload;
21
+ /** Unsaved Settings form draft per scope (session memory). */
22
+ settingsDraft: SettingsDraftStore;
18
23
  private readonly subscriptions;
19
24
  private mcpDispose;
20
25
  constructor();
@@ -27,5 +32,5 @@ export declare class LangflowerSession {
27
32
  markPristine(): void;
28
33
  buildExecutionFeed(): ExecutionFeedSnapshotPayload | null;
29
34
  static readonly sessionReadyVersion = 1;
30
- static readonly snapshotVersion = 5;
35
+ static readonly snapshotVersion = 6;
31
36
  }
@@ -2,8 +2,9 @@ import { RuntimeFacade } from '@langflower/runtime';
2
2
  import { deriveExecutionProgressStatus } from '@langflower/shared/langflower.js';
3
3
  import { Subscription } from 'rxjs';
4
4
  import { PendingPermissionAsks } from '../harness/pending-permission-asks.js';
5
+ import { emptySettingsDraftStore, } from './settings-draft-session.js';
5
6
  const SESSION_READY_VERSION = 1;
6
- const SNAPSHOT_VERSION = 5;
7
+ const SNAPSHOT_VERSION = 6;
7
8
  export class LangflowerSession {
8
9
  runtime = new RuntimeFacade({ log: true });
9
10
  /** Feed permission.ask pause/resume for the internal tool loop. */
@@ -21,6 +22,13 @@ export class LangflowerSession {
21
22
  };
22
23
  /** In-memory canvas selection, shared across tabs. `null` = nothing selected. */
23
24
  selectedNodeId = null;
25
+ /** Settings aside chrome — session memory only (not persisted to disk). */
26
+ settings = {
27
+ open: false,
28
+ scope: 'project',
29
+ };
30
+ /** Unsaved Settings form draft per scope (session memory). */
31
+ settingsDraft = emptySettingsDraftStore();
24
32
  subscriptions = new Subscription();
25
33
  mcpDispose;
26
34
  constructor() {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * In-memory Settings draft per scope (session memory, not disk).
3
+ */
4
+ import type { LangflowerConfig, LangflowerConfigDraftSnapshotPayload, LangflowerConfigScope, ProviderConnectionStatus, SettingsDraft } from '@langflower/shared/langflower.js';
5
+ export type ScopeSettingsDraft = {
6
+ readonly draft: SettingsDraft;
7
+ readonly baseline: SettingsDraft;
8
+ readonly connections: Readonly<Record<string, ProviderConnectionStatus>>;
9
+ };
10
+ export type SettingsDraftStore = {
11
+ project: ScopeSettingsDraft | undefined;
12
+ global: ScopeSettingsDraft | undefined;
13
+ };
14
+ export declare const emptySettingsDraftStore: () => SettingsDraftStore;
15
+ export declare const idleConnectionsForDraft: (draft: SettingsDraft) => Readonly<Record<string, ProviderConnectionStatus>>;
16
+ export declare const seedScopeDraft: (layerConfig: LangflowerConfig) => ScopeSettingsDraft;
17
+ /** Indexes of draft rows that should be probed (non-empty Base URL). */
18
+ export declare const providerIndexesWithBaseUrl: (draft: SettingsDraft) => readonly number[];
19
+ export declare const buildDraftSnapshot: (scope: LangflowerConfigScope, state: ScopeSettingsDraft) => LangflowerConfigDraftSnapshotPayload;
20
+ export declare const applyDraftPatch: (previous: ScopeSettingsDraft, incoming: SettingsDraft) => {
21
+ readonly next: ScopeSettingsDraft;
22
+ readonly probeIndexes: readonly number[];
23
+ };
24
+ export declare const setConnectionStatus: (state: ScopeSettingsDraft, index: number, status: ProviderConnectionStatus) => ScopeSettingsDraft;