@causyne/agent 1.10.19 → 1.10.21

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 (48) hide show
  1. package/docs/ARCHITECTURE.md +20 -0
  2. package/docs/THIRD_PARTY_NOTICES.md +44 -0
  3. package/native-skills/media/creative-studio-pipeline/SKILL.md +52 -0
  4. package/native-skills/media/photo-studio-automation/SKILL.md +53 -0
  5. package/native-skills/media/video-timeline-assembly/SKILL.md +52 -0
  6. package/package.json +1 -1
  7. package/src/agentLoop.js +4 -0
  8. package/src/appChannel.js +7 -3
  9. package/src/cliMode.js +33 -67
  10. package/src/commands.js +56 -8
  11. package/src/compiledSkills.js +15 -0
  12. package/src/computerUse/visionGrounding.js +249 -0
  13. package/src/computerUse/visionGrounding.ts +331 -0
  14. package/src/config.js +80 -11
  15. package/src/creative/apps.js +69 -0
  16. package/src/creative/apps.ts +58 -0
  17. package/src/creative/blenderBatch.js +233 -0
  18. package/src/creative/blenderBatch.ts +217 -0
  19. package/src/creative/pipeline.js +56 -0
  20. package/src/creative/pipeline.ts +82 -0
  21. package/src/creative/studioScripts.js +374 -0
  22. package/src/creative/studioScripts.ts +378 -0
  23. package/src/decisionPolicy.js +2 -0
  24. package/src/decisionPolicy.ts +1 -0
  25. package/src/dynamicCatalogue.js +270 -0
  26. package/src/dynamicCatalogue.ts +283 -0
  27. package/src/entrypoint.js +2 -1
  28. package/src/modelCatalogue.d.ts +5 -0
  29. package/src/modelCatalogue.js +49 -18
  30. package/src/providers/anthropic.js +4 -0
  31. package/src/sandbox.js +8 -1
  32. package/src/toolPacks.js +6 -3
  33. package/src/tools/computerUse.js +139 -0
  34. package/src/tools/computerUse.ts +147 -0
  35. package/src/tools/creativeApps.js +106 -0
  36. package/src/tools/creativeApps.ts +99 -0
  37. package/src/tools/desktopAct.d.ts +10 -0
  38. package/src/tools/registry.js +5 -1
  39. package/src/tools/uiForge.js +132 -0
  40. package/src/tools/uiForge.ts +145 -0
  41. package/src/tui/banner.js +3 -1
  42. package/src/tui/banner.ts +3 -1
  43. package/src/tui/dialogs.js +10 -0
  44. package/src/tui/home.js +29 -72
  45. package/src/tui/setup.js +104 -0
  46. package/src/tui/setup.ts +98 -0
  47. package/src/uiEngine/uiForge.js +211 -0
  48. package/src/uiEngine/uiForge.ts +258 -0
package/src/tui/banner.js CHANGED
@@ -237,7 +237,9 @@ function shortPath(p, maxw) {
237
237
  }
238
238
  function statusLine(info, maxw, unicode) {
239
239
  const sep = unicode ? ' · ' : ' | ';
240
- const parts = [info.version ? 'v' + String(info.version).replace(/^v/, '') : '', [info.provider, info.model].filter(Boolean).join('/'), info.mode ?? '', info.cwd ?? ''].filter(Boolean);
240
+ const providerModel = [info.provider, info.model].filter(Boolean).join('/');
241
+ const providerPart = providerModel || 'select provider';
242
+ const parts = [info.version ? 'v' + String(info.version).replace(/^v/, '') : '', providerPart, info.mode ?? '', info.cwd ?? ''].filter(Boolean);
241
243
  const fits = () => parts.join(sep).length <= maxw;
242
244
  if (!fits() && info.cwd) {
243
245
  const others = parts.slice(0, -1).join(sep).length + (parts.length > 1 ? sep.length : 0);
package/src/tui/banner.ts CHANGED
@@ -240,7 +240,9 @@ function shortPath(p: string, maxw: number): string {
240
240
  }
241
241
  function statusLine(info: BannerInfo, maxw: number, unicode: boolean): string {
242
242
  const sep = unicode ? ' · ' : ' | ';
243
- const parts = [info.version ? 'v' + String(info.version).replace(/^v/, '') : '', [info.provider, info.model].filter(Boolean).join('/'), info.mode ?? '', info.cwd ?? ''].filter(Boolean);
243
+ const providerModel = [info.provider, info.model].filter(Boolean).join('/');
244
+ const providerPart = providerModel || 'select provider';
245
+ const parts = [info.version ? 'v' + String(info.version).replace(/^v/, '') : '', providerPart, info.mode ?? '', info.cwd ?? ''].filter(Boolean);
244
246
  const fits = (): boolean => parts.join(sep).length <= maxw;
245
247
  if (!fits() && info.cwd) {
246
248
  const others = parts.slice(0, -1).join(sep).length + (parts.length > 1 ? sep.length : 0);
@@ -38,9 +38,17 @@ export class SelectLayer extends Layer {
38
38
  emptyText: options.emptyText
39
39
  });
40
40
  this.onPick = options.onPick || null;
41
+ this.settled = false;
41
42
  this.footer = options.footer || dialogHint();
42
43
  }
43
44
 
45
+ /** Dismissal must resolve pending picker flows, just like text inputs. */
46
+ answer() {
47
+ if (this.settled) return;
48
+ this.settled = true;
49
+ if (this.onPick) this.onPick(null);
50
+ }
51
+
44
52
  preferredHeight() {
45
53
  // filter row + rule + one row per item (+ its category header) + frame.
46
54
  const categories = new Set(this.list.filtered.map((entry) => entry.item.category).filter(Boolean));
@@ -66,7 +74,9 @@ export class SelectLayer extends Layer {
66
74
 
67
75
  if (action === 'select') {
68
76
  const item = this.list.current;
77
+ this.settled = true;
69
78
  const result = this.onPick ? this.onPick(item) : null;
79
+ if (result === 'keep') this.settled = false;
70
80
  return result === 'keep' ? 'handled' : 'close';
71
81
  }
72
82
  if (action === 'cancel') return 'close';
package/src/tui/home.js CHANGED
@@ -2,15 +2,21 @@
2
2
 
3
3
  import { SelectLayer, InputLayer } from './dialogs.js';
4
4
  import { paint, dim, bold, badge, GLYPHS } from './theme.js';
5
+ import { runSetup } from './setup.js';
5
6
  import { DEFAULT_MODEL_BY_PROVIDER } from '../modelCatalogue.js';
6
7
 
7
8
  /** Local runtime and provider summary. */
8
9
  export function homeStatusLine({ config = {} } = {}) {
9
- const model = config.model ? `${config.provider || 'default'}/${config.model}` : (config.provider || 'no provider');
10
- const configured = config.apiKey || config.provider === 'ollama';
10
+ const hasProvider = Boolean(config.provider && config.provider !== 'unconfigured');
11
+ const model = hasProvider
12
+ ? (config.model ? `${config.provider}/${config.model}` : config.provider)
13
+ : 'no provider configured';
14
+ const configured = Boolean((hasProvider && config.apiKey) || config.provider === 'ollama');
11
15
  return ' ' + [
12
16
  paint('success', `${GLYPHS.bullet} local desktop`),
13
- configured ? dim(model) : paint('warn', `${model} — no key`)
17
+ configured
18
+ ? dim(model)
19
+ : (hasProvider ? paint('warn', `${model} — no key`) : paint('warn', 'no provider configured — run /setup'))
14
20
  ].join(dim(` ${GLYPHS.dot} `));
15
21
  }
16
22
 
@@ -41,7 +47,7 @@ export const PRIMARY_ACTIONS = [
41
47
  kind: 'stay',
42
48
  run: async () => (await import('../reporting.js')).buildUsageText()
43
49
  },
44
- { id: 'config', label: 'config', hint: 'provider, model, API key', kind: 'config' },
50
+ { id: 'config', label: 'setup', hint: 'provider · endpoint · model · permissions', kind: 'config' },
45
51
  { id: 'more', label: 'more…', hint: 'sessions · model · approvals · tools', kind: 'more' }
46
52
  ];
47
53
 
@@ -70,6 +76,13 @@ export const MORE_ACTIONS = [
70
76
  // be the one path around the gate.
71
77
  command: '/undo'
72
78
  },
79
+ {
80
+ id: 'creative', label: 'creative apps', category: 'tools', hint: 'check installed apps and adapter readiness', kind: 'stay',
81
+ run: async () => {
82
+ const { creativeAppStatus } = await import('../creative/apps.js');
83
+ return (await creativeAppStatus()).map(app => `${app.label}: ${app.available ? 'available' : 'not connected'}\n ${app.detail}`).join('\n');
84
+ }
85
+ },
73
86
  { id: 'mcp', label: 'mcp', category: 'tools', hint: 'manage MCP servers', kind: 'mcp' },
74
87
  { id: 'help', label: 'commands', category: 'tools', hint: 'show every slash command', kind: 'command', command: '/help' },
75
88
  {
@@ -111,16 +124,8 @@ export function homeHint() {
111
124
  export { badge };
112
125
 
113
126
  // ---------------------------------------------------------------------------
114
- // The two flows that need a keyboard
127
+ // Provider setup and MCP navigation
115
128
  //
116
- // Both of these used inquirer, which takes stdin while ChatPrompt is already
117
- // holding it in raw mode — the two contend and the question lands on a
118
- // half-drawn prompt. Inside home there IS a live region and a modal stack, so
119
- // they can be asked properly. `causyne config` invoked on its own still uses the
120
- // inquirer wizard, because standalone there is no live region to draw into and
121
- // building a second one is how the deleted alt-screen shell started.
122
- // ---------------------------------------------------------------------------
123
-
124
129
  /** Provider choices for local CLI configuration. */
125
130
  // Suggested models come from src/modelCatalogue.js rather than being repeated
126
131
  // here. This list had drifted to `gpt-4o` and `gemini-2.5-pro` — two
@@ -140,65 +145,17 @@ export const PROVIDERS = [
140
145
  { id: 'ollama', label: 'Ollama', hint: 'local / offline' }
141
146
  ].map((entry) => ({ ...entry, model: DEFAULT_MODEL_BY_PROVIDER[entry.id] ?? '' }));
142
147
 
143
- /**
144
- * Ask for provider, then model, then key — each in the prompt's own panel.
145
- *
146
- * Resolves when the last answer is in, or when any step is dismissed. Dismissing
147
- * cancels the whole flow rather than saving a half-configuration, because a
148
- * provider saved without its key leaves the agent in a state that fails on the
149
- * next turn with an error about credentials rather than about the cancel.
150
- */
151
- export function configureInPrompt(prompt, { loadConfig, saveConfig }) {
152
- return new Promise((resolve) => {
153
- const config = loadConfig();
154
-
155
- prompt.openLayer(new SelectLayer({
156
- title: 'provider',
157
- items: PROVIDERS.map((p) => ({ id: p.id, label: p.label, description: p.hint, value: p })),
158
- onPick: (item) => {
159
- if (!item) { resolve(null); return; }
160
- const provider = item.value;
161
- askModel(provider);
162
- }
163
- }));
164
-
165
- function askModel(provider) {
166
- const suggested = (config.provider === provider.id && config.model) ? config.model : provider.model;
167
- prompt.openLayer(new InputLayer({
168
- title: `model for ${provider.label}`,
169
- lines: [
170
- 'Model ids change often. Type the one you want, or accept the suggestion.'
171
- ],
172
- value: suggested,
173
- placeholder: 'model id',
174
- onSubmit: (model) => askKey(provider, (model || '').trim() || suggested),
175
- onCancel: () => resolve(null)
176
- }));
177
- }
178
-
179
- function askKey(provider, model) {
180
- prompt.openLayer(new InputLayer({
181
- title: 'API key',
182
- lines: [
183
- `${provider.label} ${GLYPHS.arrow} ${model}`,
184
- '',
185
- 'Stored encrypted in ~/.causyne/config.json. It never leaves this machine.'
186
- ],
187
- mask: true,
188
- value: '',
189
- placeholder: 'paste your key (blank keeps the current one)',
190
- onSubmit: (apiKey) => {
191
- const next = loadConfig();
192
- next.provider = provider.id;
193
- next.model = model;
194
- if (apiKey) next.apiKey = apiKey;
195
- saveConfig(next);
196
- resolve({ provider: provider.id, model });
197
- },
198
- onCancel: () => resolve(null)
199
- }));
200
- }
201
- });
148
+ /** Run the shared setup transaction inside the existing prompt. */
149
+ export function configureInPrompt(prompt, store) {
150
+ return runSetup({
151
+ select: (options) => new Promise((resolve) => {
152
+ const layer = new SelectLayer({ ...options, onPick: resolve });
153
+ prompt.openLayer(layer);
154
+ }),
155
+ text: (options) => new Promise((resolve) => {
156
+ prompt.openLayer(new InputLayer({ ...options, onSubmit: resolve, onCancel: () => resolve(null) }));
157
+ })
158
+ }, PROVIDERS, store);
202
159
  }
203
160
 
204
161
  /** The MCP server list, as something you can act on rather than read. */
@@ -0,0 +1,104 @@
1
+ const string = (value) => typeof value === 'string' ? value : '';
2
+ const customProviders = new Set(['openai-compatible', 'dashscope-byok', 'ollama']);
3
+ const defaults = {
4
+ ollama: 'http://localhost:11434/api/chat',
5
+ 'dashscope-byok': 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions'
6
+ };
7
+ export function validateEndpoint(value) {
8
+ try {
9
+ const url = new URL(value);
10
+ if (url.username || url.password || url.search || url.hash)
11
+ return 'Use an endpoint without credentials, query parameters, or a fragment.';
12
+ const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
13
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback))
14
+ return 'Use HTTPS, or HTTP on localhost for a local model.';
15
+ return null;
16
+ }
17
+ catch {
18
+ return 'Enter a complete endpoint URL.';
19
+ }
20
+ }
21
+ export async function runSetup(ui, providers, store, workspace = process.cwd()) {
22
+ const original = store.loadConfig();
23
+ let draft = { ...original };
24
+ async function text(title, lines, value = '', mask = false, validate = () => null) {
25
+ let error = null;
26
+ for (;;) {
27
+ const answer = await ui.text({ title, lines: error ? [...lines, error] : lines, value,
28
+ placeholder: mask ? 'paste key' : '', mask });
29
+ if (answer === null)
30
+ return null;
31
+ const cleaned = answer.trim();
32
+ error = /[\x00-\x1f\x7f]/.test(cleaned) ? 'Use a single line without control characters.' : validate(cleaned);
33
+ if (!error)
34
+ return cleaned;
35
+ value = mask ? '' : cleaned;
36
+ }
37
+ }
38
+ for (;;) {
39
+ const selected = await ui.select({ title: 'Setup 1/5 · provider', items: providers.map(p => ({ id: p.id, label: p.label, description: p.hint })) });
40
+ if (!selected)
41
+ return null;
42
+ const provider = providers.find(p => p.id === selected.id);
43
+ if (!provider)
44
+ return null;
45
+ const sameProvider = draft.provider === provider.id;
46
+ let baseUrl = '';
47
+ if (customProviders.has(provider.id)) {
48
+ const answer = await text('Setup 2/5 · endpoint', ['Full chat endpoint URL, including /v1/chat/completions or /api/chat.',
49
+ 'Credentials are sent to this endpoint when you use the model.'], (sameProvider ? string(draft.baseUrl) : '') || defaults[provider.id] || '', false, validateEndpoint);
50
+ // No network request is made by setup.
51
+ if (answer === null)
52
+ return null;
53
+ baseUrl = answer;
54
+ }
55
+ const model = await text('Setup 3/5 · model', ['Enter the model ID supported by your provider.'], (sameProvider ? string(draft.model) : '') || provider.model, false, value => value ? null : 'A model ID is required.');
56
+ if (model === null)
57
+ return null;
58
+ const unchangedEndpoint = sameProvider && (string(draft.baseUrl) || defaults[provider.id] || '') === baseUrl;
59
+ // Fixed endpoints have no editable URL. A changed endpoint never inherits a key.
60
+ const keepKey = sameProvider && (!customProviders.has(provider.id) || unchangedEndpoint);
61
+ let apiKey = '';
62
+ if (provider.id !== 'ollama') {
63
+ const oldKey = keepKey ? string(draft.apiKey) : '';
64
+ const answer = await text('Setup 3/5 · credentials', ['Stored encrypted locally; used to authenticate with your selected provider.',
65
+ oldKey ? 'Blank keeps the key for this same provider and endpoint.' : 'Enter a key for this provider.',
66
+ provider.id === 'openai-compatible' ? 'Blank is allowed for an endpoint that needs no authentication.' : ''], '', true, value => value || oldKey || provider.id === 'openai-compatible' ? null : 'An API key is required.');
67
+ if (answer === null)
68
+ return null;
69
+ apiKey = answer || oldKey;
70
+ }
71
+ const mode = await ui.select({ title: 'Setup 4/5 · autonomy', items: [
72
+ { id: 'B', label: 'B · Supervised (Standard)', description: 'Recommended. Approval for dangerous actions; routine work continues.' },
73
+ { id: 'A', label: 'A · Ask first (Strict)', description: 'Explicit confirmation before every tool call.' },
74
+ { id: 'C', label: 'C · Autonomous (High Velocity)', description: 'Fewer interruptions; mandatory safety and decision gates remain.' }
75
+ ] });
76
+ if (!mode || !['A', 'B', 'C'].includes(mode.id))
77
+ return null;
78
+ draft = { ...draft, provider: provider.id, model, apiKey, autonomyMode: mode.id };
79
+ if (baseUrl)
80
+ draft.baseUrl = baseUrl;
81
+ else
82
+ delete draft.baseUrl;
83
+ const review = await ui.select({ title: 'Setup 5/5 · review', items: [
84
+ { id: 'save', label: 'Save configuration', description: `${provider.label} · ${model} · Mode ${mode.id}` },
85
+ { id: 'edit', label: 'Edit choices', description: baseUrl || 'Provider default endpoint' },
86
+ { id: 'cancel', label: 'Cancel without saving', description: `Workspace: ${workspace}` }
87
+ ] });
88
+ if (!review || review.id === 'cancel')
89
+ return null;
90
+ if (review.id === 'edit')
91
+ continue;
92
+ if (review.id !== 'save')
93
+ return null;
94
+ // Preserve unrelated settings changed while the dialog was open.
95
+ const next = { ...store.loadConfig(), provider: draft.provider, model, apiKey, autonomyMode: mode.id };
96
+ const saved = next;
97
+ if (baseUrl)
98
+ saved.baseUrl = baseUrl;
99
+ else
100
+ delete saved.baseUrl;
101
+ store.saveConfig(saved);
102
+ return { provider: provider.id, model };
103
+ }
104
+ }
@@ -0,0 +1,98 @@
1
+ /** Shared setup transaction for the home panel and standalone CLI. */
2
+ export interface SetupChoice { id: string; label: string; description?: string }
3
+ export interface SetupProvider { id: string; label: string; hint: string; model: string }
4
+ export interface SetupUI {
5
+ select(options: { title: string; items: SetupChoice[] }): Promise<SetupChoice | null>;
6
+ text(options: { title: string; lines: string[]; value: string; placeholder: string; mask: boolean }): Promise<string | null>;
7
+ }
8
+ type Config = Record<string, unknown>;
9
+ const string = (value: unknown): string => typeof value === 'string' ? value : '';
10
+ const customProviders = new Set(['openai-compatible', 'dashscope-byok', 'ollama']);
11
+ const defaults: Record<string, string> = {
12
+ ollama: 'http://localhost:11434/api/chat',
13
+ 'dashscope-byok': 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions'
14
+ };
15
+
16
+ export function validateEndpoint(value: string): string | null {
17
+ try {
18
+ const url = new URL(value);
19
+ if (url.username || url.password || url.search || url.hash) return 'Use an endpoint without credentials, query parameters, or a fragment.';
20
+ const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
21
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) return 'Use HTTPS, or HTTP on localhost for a local model.';
22
+ return null;
23
+ } catch { return 'Enter a complete endpoint URL.'; }
24
+ }
25
+
26
+ export async function runSetup(ui: SetupUI, providers: SetupProvider[], store: {
27
+ loadConfig(): Config; saveConfig(config: Config): void;
28
+ }, workspace = process.cwd()): Promise<{ provider: string; model: string } | null> {
29
+ const original = store.loadConfig();
30
+ let draft: Config = { ...original };
31
+ async function text(title: string, lines: string[], value = '', mask = false,
32
+ validate: (answer: string) => string | null = () => null): Promise<string | null> {
33
+ let error: string | null = null;
34
+ for (;;) {
35
+ const answer = await ui.text({ title, lines: error ? [...lines, error] : lines, value,
36
+ placeholder: mask ? 'paste key' : '', mask });
37
+ if (answer === null) return null;
38
+ const cleaned = answer.trim();
39
+ error = /[\x00-\x1f\x7f]/.test(cleaned) ? 'Use a single line without control characters.' : validate(cleaned);
40
+ if (!error) return cleaned;
41
+ value = mask ? '' : cleaned;
42
+ }
43
+ }
44
+ for (;;) {
45
+ const selected = await ui.select({ title: 'Setup 1/5 · provider', items: providers.map(p => ({ id: p.id, label: p.label, description: p.hint })) });
46
+ if (!selected) return null;
47
+ const provider = providers.find(p => p.id === selected.id);
48
+ if (!provider) return null;
49
+ const sameProvider = draft.provider === provider.id;
50
+ let baseUrl = '';
51
+ if (customProviders.has(provider.id)) {
52
+ const answer = await text('Setup 2/5 · endpoint', ['Full chat endpoint URL, including /v1/chat/completions or /api/chat.',
53
+ 'Credentials are sent to this endpoint when you use the model.'],
54
+ (sameProvider ? string(draft.baseUrl) : '') || defaults[provider.id] || '', false, validateEndpoint);
55
+ // No network request is made by setup.
56
+ if (answer === null) return null;
57
+ baseUrl = answer;
58
+ }
59
+ const model = await text('Setup 3/5 · model', ['Enter the model ID supported by your provider.'],
60
+ (sameProvider ? string(draft.model) : '') || provider.model, false, value => value ? null : 'A model ID is required.');
61
+ if (model === null) return null;
62
+ const unchangedEndpoint = sameProvider && (string(draft.baseUrl) || defaults[provider.id] || '') === baseUrl;
63
+ // Fixed endpoints have no editable URL. A changed endpoint never inherits a key.
64
+ const keepKey = sameProvider && (!customProviders.has(provider.id) || unchangedEndpoint);
65
+ let apiKey = '';
66
+ if (provider.id !== 'ollama') {
67
+ const oldKey = keepKey ? string(draft.apiKey) : '';
68
+ const answer = await text('Setup 3/5 · credentials', ['Stored encrypted locally; used to authenticate with your selected provider.',
69
+ oldKey ? 'Blank keeps the key for this same provider and endpoint.' : 'Enter a key for this provider.',
70
+ provider.id === 'openai-compatible' ? 'Blank is allowed for an endpoint that needs no authentication.' : ''], '', true,
71
+ value => value || oldKey || provider.id === 'openai-compatible' ? null : 'An API key is required.');
72
+ if (answer === null) return null;
73
+ apiKey = answer || oldKey;
74
+ }
75
+ const mode = await ui.select({ title: 'Setup 4/5 · autonomy', items: [
76
+ { id: 'B', label: 'B · Supervised (Standard)', description: 'Recommended. Approval for dangerous actions; routine work continues.' },
77
+ { id: 'A', label: 'A · Ask first (Strict)', description: 'Explicit confirmation before every tool call.' },
78
+ { id: 'C', label: 'C · Autonomous (High Velocity)', description: 'Fewer interruptions; mandatory safety and decision gates remain.' }
79
+ ] });
80
+ if (!mode || !['A', 'B', 'C'].includes(mode.id)) return null;
81
+ draft = { ...draft, provider: provider.id, model, apiKey, autonomyMode: mode.id };
82
+ if (baseUrl) draft.baseUrl = baseUrl; else delete draft.baseUrl;
83
+ const review = await ui.select({ title: 'Setup 5/5 · review', items: [
84
+ { id: 'save', label: 'Save configuration', description: `${provider.label} · ${model} · Mode ${mode.id}` },
85
+ { id: 'edit', label: 'Edit choices', description: baseUrl || 'Provider default endpoint' },
86
+ { id: 'cancel', label: 'Cancel without saving', description: `Workspace: ${workspace}` }
87
+ ] });
88
+ if (!review || review.id === 'cancel') return null;
89
+ if (review.id === 'edit') continue;
90
+ if (review.id !== 'save') return null;
91
+ // Preserve unrelated settings changed while the dialog was open.
92
+ const next = { ...store.loadConfig(), provider: draft.provider, model, apiKey, autonomyMode: mode.id };
93
+ const saved: Config = next;
94
+ if (baseUrl) saved.baseUrl = baseUrl; else delete saved.baseUrl;
95
+ store.saveConfig(saved);
96
+ return { provider: provider.id, model };
97
+ }
98
+ }
@@ -0,0 +1,211 @@
1
+ /*
2
+ * Copyright (c) 2011-2024 Thesys Inc.
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Adapted for Causyne UI Forge — Generative Component & Design Engine.
6
+ */
7
+ /**
8
+ * Generate semantic, modern HTML + CSS component code under Causyne UI Forge.
9
+ */
10
+ export function generateHtmlComponent(spec) {
11
+ const isDark = spec.theme !== 'light';
12
+ const cardBg = isDark ? 'rgba(30, 41, 59, 0.75)' : 'rgba(255, 255, 255, 0.95)';
13
+ const border = isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.1)';
14
+ const text = isDark ? '#f8fafc' : '#0f172a';
15
+ const textMuted = isDark ? '#94a3b8' : '#64748b';
16
+ const accent = '#6366f1';
17
+ const accentHover = '#4f46e5';
18
+ const css = `
19
+ /* Causyne UI Forge Generated Styles - Responsive, Accessible & Performant */
20
+ .causyne-ui-root {
21
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
22
+ color: ${text};
23
+ line-height: 1.5;
24
+ box-sizing: border-box;
25
+ }
26
+ .causyne-ui-root * { box-sizing: border-box; margin: 0; padding: 0; }
27
+ .causyne-ui-card {
28
+ background: ${cardBg};
29
+ backdrop-filter: blur(12px);
30
+ border: 1px solid ${border};
31
+ border-radius: 12px;
32
+ padding: 24px;
33
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
34
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
35
+ }
36
+ .causyne-ui-card:hover {
37
+ transform: translateY(-2px);
38
+ box-shadow: 0 20px 30px -10px rgba(0, 0, 0, 0.3);
39
+ }
40
+ .causyne-ui-header { margin-bottom: 16px; }
41
+ .causyne-ui-title { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.02em; }
42
+ .causyne-ui-desc { font-size: 0.875rem; color: ${textMuted}; margin-top: 4px; }
43
+ .causyne-ui-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin: 16px 0; }
44
+ .causyne-ui-stat-box {
45
+ background: rgba(255, 255, 255, 0.03);
46
+ border: 1px solid ${border};
47
+ border-radius: 8px;
48
+ padding: 16px;
49
+ }
50
+ .causyne-ui-stat-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: ${textMuted}; }
51
+ .causyne-ui-stat-value { font-size: 1.75rem; font-weight: 700; color: ${accent}; margin-top: 4px; }
52
+ .causyne-ui-btn {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ padding: 8px 16px;
57
+ border-radius: 6px;
58
+ font-size: 0.875rem;
59
+ font-weight: 500;
60
+ cursor: pointer;
61
+ border: 1px solid transparent;
62
+ transition: all 0.15s ease;
63
+ }
64
+ .causyne-ui-btn-primary { background: ${accent}; color: #ffffff; }
65
+ .causyne-ui-btn-primary:hover { background: ${accentHover}; }
66
+ .causyne-ui-btn-secondary { background: transparent; border-color: ${border}; color: ${text}; }
67
+ .causyne-ui-btn-secondary:hover { background: rgba(255, 255, 255, 0.05); }
68
+ .causyne-ui-actions { display: flex; gap: 8px; margin-top: 20px; justify-content: flex-end; }
69
+ `.trim();
70
+ let html = '';
71
+ const title = spec.title || 'Component';
72
+ const desc = spec.description || '';
73
+ if (spec.type === 'dashboard-stats' || (spec.items && spec.items.length > 0)) {
74
+ const itemsHtml = (spec.items || [])
75
+ .map((item) => `
76
+ <div class="causyne-ui-stat-box">
77
+ <div class="causyne-ui-stat-label">${escapeHtml(item.label)}</div>
78
+ <div class="causyne-ui-stat-value">${escapeHtml(String(item.value ?? '—'))}</div>
79
+ ${item.description ? `<div class="causyne-ui-desc">${escapeHtml(item.description)}</div>` : ''}
80
+ </div>`)
81
+ .join('\n');
82
+ html = `
83
+ <div class="causyne-ui-root">
84
+ <div class="causyne-ui-card">
85
+ <div class="causyne-ui-header">
86
+ <h3 class="causyne-ui-title">${escapeHtml(title)}</h3>
87
+ ${desc ? `<p class="causyne-ui-desc">${escapeHtml(desc)}</p>` : ''}
88
+ </div>
89
+ <div class="causyne-ui-grid">
90
+ ${itemsHtml}
91
+ </div>
92
+ ${renderActions(spec.actions)}
93
+ </div>
94
+ </div>`.trim();
95
+ }
96
+ else {
97
+ html = `
98
+ <div class="causyne-ui-root">
99
+ <div class="causyne-ui-card">
100
+ <div class="causyne-ui-header">
101
+ <h3 class="causyne-ui-title">${escapeHtml(title)}</h3>
102
+ ${desc ? `<p class="causyne-ui-desc">${escapeHtml(desc)}</p>` : ''}
103
+ </div>
104
+ ${renderActions(spec.actions)}
105
+ </div>
106
+ </div>`.trim();
107
+ }
108
+ return { html, css };
109
+ }
110
+ function renderActions(actions) {
111
+ if (!actions || actions.length === 0)
112
+ return '';
113
+ const btns = actions
114
+ .map((a) => `<button class="causyne-ui-btn ${a.primary ? 'causyne-ui-btn-primary' : 'causyne-ui-btn-secondary'}" data-action="${escapeHtml(a.action)}">${escapeHtml(a.label)}</button>`)
115
+ .join('\n ');
116
+ return `
117
+ <div class="causyne-ui-actions">
118
+ ${btns}
119
+ </div>`;
120
+ }
121
+ function escapeHtml(str) {
122
+ return String(str || '')
123
+ .replace(/&/g, '&amp;')
124
+ .replace(/</g, '&lt;')
125
+ .replace(/>/g, '&gt;')
126
+ .replace(/"/g, '&quot;');
127
+ }
128
+ /**
129
+ * Generate full self-contained preview document.
130
+ */
131
+ export function generatePreviewHtml(html, css, theme = 'dark') {
132
+ const bg = theme === 'dark' ? '#0b0f19' : '#f1f5f9';
133
+ return `<!DOCTYPE html>
134
+ <html lang="en">
135
+ <head>
136
+ <meta charset="UTF-8">
137
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
138
+ <title>Causyne UI Forge Preview</title>
139
+ <style>
140
+ body {
141
+ margin: 0;
142
+ padding: 40px 20px;
143
+ min-height: 100vh;
144
+ background: ${bg};
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ box-sizing: border-box;
149
+ }
150
+ ${css}
151
+ </style>
152
+ </head>
153
+ <body>
154
+ ${html}
155
+ </body>
156
+ </html>`;
157
+ }
158
+ /**
159
+ * Generate React + Tailwind component code under Causyne UI Forge.
160
+ */
161
+ export function generateReactComponent(spec) {
162
+ const compName = (spec.name || 'CausyneCard').replace(/[^a-zA-Z0-9]/g, '');
163
+ const title = spec.title || 'Component';
164
+ const desc = spec.description || '';
165
+ return `import React from 'react';
166
+
167
+ export interface ${compName}Props {
168
+ title?: string;
169
+ description?: string;
170
+ }
171
+
172
+ export const ${compName}: React.FC<${compName}Props> = ({
173
+ title = "${title}",
174
+ description = "${desc}"
175
+ }) => {
176
+ return (
177
+ <div className="rounded-xl border border-slate-700/60 bg-slate-900/80 p-6 backdrop-blur-md shadow-xl transition-all hover:shadow-2xl">
178
+ <div className="mb-4">
179
+ <h3 className="text-xl font-semibold tracking-tight text-white">{title}</h3>
180
+ {description && <p className="mt-1 text-sm text-slate-400">{description}</p>}
181
+ </div>
182
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 my-4">
183
+ ${(spec.items || [])
184
+ .map((item) => `
185
+ <div key="${item.id || item.label}" className="rounded-lg border border-slate-800 bg-slate-800/40 p-4">
186
+ <div className="text-xs font-medium uppercase tracking-wider text-slate-400">${item.label}</div>
187
+ <div className="mt-1 text-2xl font-bold text-indigo-400">${item.value ?? '—'}</div>
188
+ ${item.description ? `<p className="mt-1 text-xs text-slate-500">${item.description}</p>` : ''}
189
+ </div>`)
190
+ .join('')}
191
+ </div>
192
+ ${(spec.actions && spec.actions.length > 0)
193
+ ? `
194
+ <div className="mt-6 flex justify-end gap-3">
195
+ ${spec.actions
196
+ .map((a) => `
197
+ <button className="px-4 py-2 text-sm font-medium rounded-lg transition-colors ${a.primary
198
+ ? 'bg-indigo-600 text-white hover:bg-indigo-500'
199
+ : 'border border-slate-700 text-slate-300 hover:bg-slate-800'}">
200
+ ${a.label}
201
+ </button>`)
202
+ .join('')}
203
+ </div>`
204
+ : ''}
205
+ </div>
206
+ );
207
+ };
208
+
209
+ export default ${compName};
210
+ `;
211
+ }