@atmem/architect 0.2.2 → 0.2.3

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.
@@ -64,6 +64,8 @@ class RecoveredStart:
64
64
 
65
65
  class EngineeringBackend(Protocol):
66
66
  name: str
67
+ @property
68
+ def session_count(self) -> int: ...
67
69
  async def create(self,scope: ProjectScope,workspace: str,selection: ModelSelection) -> str: ...
68
70
  async def resume(self,scope: ProjectScope,handle: str,workspace: str,selection: ModelSelection) -> None: ...
69
71
  async def observe(self,scope: ProjectScope,handle: str,turn: str) -> BackendEvent | None: ...
@@ -133,10 +133,13 @@ class DockerValidationWorker:
133
133
  env=env,capture_output=True,text=True,timeout=15)
134
134
  if inspect.returncode!=0 or inspect.stdout.strip()!=spec.image:
135
135
  raise RuntimeError('The reviewed local image/platform is unavailable; no image was pulled')
136
- platform=subprocess.run([*prefix,'image','inspect','--platform',spec.platform,spec.image,'--format','{{.Os}}/{{.Architecture}} {{.Id}}'],
136
+ # A registered config image ID identifies one architecture, not a
137
+ # multi-platform tag/index. Inspect that exact cached config directly;
138
+ # --platform inspection requires a newer daemon API unnecessarily.
139
+ platform=subprocess.run([*prefix,'image','inspect',spec.image,'--format','{{.Os}}/{{.Architecture}} {{.Id}}'],
137
140
  env=env,capture_output=True,text=True,timeout=15)
138
141
  selection=platform.stdout.strip().split()
139
- if platform.returncode!=0 or len(selection)!=2 or selection[0]!=spec.platform or not re.fullmatch(r'sha256:[a-f0-9]{64}',selection[1]):
142
+ if platform.returncode!=0 or len(selection)!=2 or selection[0]!=spec.platform or selection[1]!=spec.image:
140
143
  raise RuntimeError('The reviewed cached platform is unavailable; no fallback or image pull was used')
141
144
  browser=spec.isolation_profile=='chromium-userns-v1'
142
145
  postgres=spec.isolation_profile=='postgresql-local-v1'
@@ -71,7 +71,8 @@ def validate(payload):
71
71
  env = {'PATH': os.getenv('PATH', ''), 'HOME': folder, 'TMPDIR': folder, 'TF_IN_AUTOMATION': '1', 'CHECKPOINT_DISABLE': '1', 'AWS_EC2_METADATA_DISABLED': 'true'}
72
72
  cache=Path(setting('PROVIDER_CACHE') or ROOT/'.local/provider-cache')
73
73
  if cache.is_dir():
74
- env['TF_PLUGIN_CACHE_DIR']=str(cache)
74
+ writable_cache=Path(folder)/'.provider-cache';writable_cache.mkdir()
75
+ env['TF_PLUGIN_CACHE_DIR']=str(writable_cache)
75
76
  mirror=Path(folder)/'.atmem-provider-mirror.tfrc'
76
77
  mirror.write_text('provider_installation { filesystem_mirror { path = '+json.dumps(str(cache.resolve()))+' } }\n')
77
78
  env['TF_CLI_CONFIG_FILE']=str(mirror)
@@ -1,6 +1,6 @@
1
1
  import {BrainActivity} from './BrainActivity.jsx';
2
2
  import React,{useEffect,useRef,useState} from 'react';
3
- import {engineeringCall,engineeringConversation,mergeEngineeringEvents} from './engineering-client.mjs';
3
+ import {engineeringCall,engineeringConversation,mergeEngineeringEvents,engineeringSelectionSupported} from './engineering-client.mjs';
4
4
  import './engineering.css';
5
5
  import {ArtifactChecks} from './ArtifactChecks.jsx';
6
6
  import {verifyArtifactWork} from './artifact-work.mjs';
@@ -60,7 +60,7 @@ export function EngineeringPanel({projectId,session,connection,saveProject,onCon
60
60
  try{await operation();}catch(e){if(live.current)setError(e.message);}finally{actionLock.current=false;if(live.current)setBusy(false);}
61
61
  }
62
62
  useEffect(()=>{onBusyChange?.(busy||loading||['running','starting'].includes(job?.state));return()=>onBusyChange?.(false);},[busy,loading,job?.state,onBusyChange]);
63
- const exactSelection=connection?.provider==='codex'&&connection.model&&connection.model!=='default'&&connection.effort;
63
+ const exactSelection=engineeringSelectionSupported(session,connection);
64
64
  const running=job?.state==='running',uncertain=job?.state==='starting';
65
65
  const lastProgress=events.filter(event=>event.runId===job?.run&&['progress','message'].includes(event.kind)).at(-1);
66
66
  const [lastReceived,setLastReceived]=useState(null),[now,setNow]=useState(Date.now());
@@ -3,10 +3,10 @@ import {FinalChecklist} from './FinalChecklist.jsx';
3
3
  import {BrainActivity} from './BrainActivity.jsx';
4
4
  import {completeProjectZip} from './final-package.mjs';
5
5
  import React,{useEffect,useState,useRef} from 'react';
6
- import {engineeringCall,verifiedHandoffDownload} from './engineering-client.mjs';
6
+ import {engineeringCall,verifiedHandoffDownload,engineeringDeliveryNeedsRefresh} from './engineering-client.mjs';
7
7
  import {ArtifactChecks} from './ArtifactChecks.jsx';
8
8
 
9
- export function ImplementationDelivery({project,session,onBuild,onSave,contextBusy=false,designBundle,onDesignDownload,onDownloaded,checkReceipt,onExplore,onNavigate}){
9
+ export function ImplementationDelivery({project,session,workStatus,onBuild,onSave,contextBusy=false,designBundle,onDesignDownload,onDownloaded,checkReceipt,onExplore,onNavigate}){
10
10
  const [orcaResult,setOrcaResult]=useState(null);
11
11
  const projectRef=useRef(project);projectRef.current=project;
12
12
  const [prepared,setPrepared]=useState(null),[preparing,setPreparing]=useState(false);
@@ -18,7 +18,11 @@ export function ImplementationDelivery({project,session,onBuild,onSave,contextBu
18
18
  if(!controller.signal.aborted)setLoaded({project,report:value});
19
19
  }).catch(e=>{if(!controller.signal.aborted){if(e.status===404&&e.message.startsWith('Save or restore this project'))setLoaded({project,report:{artifact:null}});else setError(e.message);}});
20
20
  return()=>controller.abort();
21
- },[project,session.token,retry]);
21
+ },[project,session.token,retry,workStatus?.run,workStatus?.state,workStatus?.last_status]);
22
+ useEffect(()=>{
23
+ if(!engineeringDeliveryNeedsRefresh(report,workStatus))return;
24
+ const timer=setTimeout(()=>setRetry(value=>value+1),2500);return()=>clearTimeout(timer);
25
+ },[report,workStatus?.run,workStatus?.state]);
22
26
  useEffect(()=>{
23
27
  if(!report?.canPackage||designBundle?.project!==project){setPrepared(null);return;}
24
28
  let stopped=false;const controller=new AbortController();setPreparing(true);setError('');
@@ -37,7 +37,7 @@ function Preview({name,content}){
37
37
  if(name.endsWith('.md'))return <Document text={content}/>;
38
38
  return <CodePreview className="output-code" content={content} name={name}/>;
39
39
  }
40
- export function OutputsView({project,run,environment,session,onDownload,onBack,emulation,onDiscuss,discussionBusy,onBuild,onSave,onDownloaded,onReview,checkReceipt,onNavigate}){
40
+ export function OutputsView({project,run,environment,session,workStatus,onDownload,onBack,emulation,onDiscuss,discussionBusy,onBuild,onSave,onDownloaded,onReview,checkReceipt,onNavigate}){
41
41
  const [exploration,setExploration]=useState(null);
42
42
  const projectRef=useRef(project);projectRef.current=project;
43
43
  const [packing,setPacking]=useState(false);
@@ -60,7 +60,7 @@ export function OutputsView({project,run,environment,session,onDownload,onBack,e
60
60
  <header className="doc-head"><h2>Delivery</h2></header>
61
61
  <button className="link" onClick={onReview||onBack}>Back to Review</button>
62
62
  {isReadyExample(project)&&<ReadyExampleChecklist/>}
63
- {session?.engineeringEnabled&&(isReadyExample(project)?<details><summary>Checks for your edited project</summary><ImplementationDelivery key={project.id} project={project} session={session} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/></details>:<ImplementationDelivery key={project.id} project={project} session={session} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/>)}
63
+ {session?.engineeringEnabled&&(isReadyExample(project)?<details><summary>Checks for your edited project</summary><ImplementationDelivery key={project.id} project={project} session={session} workStatus={workStatus} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/></details>:<ImplementationDelivery key={project.id} project={project} session={session} workStatus={workStatus} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/>)}
64
64
  <section aria-label="Project file explorer"><h3>Explore files</h3>
65
65
 
66
66
  {!session?.engineeringEnabled&&<><p>Design package · no implementation generated.</p><button className="btn" disabled={!bundle||loading||!!error||packing} onClick={downloadDesign}>{packing?'Preparing ZIP…':'Download project ZIP'}</button></>}
@@ -3,7 +3,7 @@ import receipt from './reference-example/receipt.json';
3
3
  export function ReadyExampleDownload({compact=false}){
4
4
  const [busy,setBusy]=useState(false),[error,setError]=useState('');
5
5
  async function download(){setBusy(true);setError('');try{
6
- const response=await fetch(receipt.url);if(!response.ok)throw Error('Example download unavailable. Retry.');
6
+ const response=await fetch(import.meta.env.BASE_URL+receipt.url.replace(/^\//,''));if(!response.ok)throw Error('Example download unavailable. Retry.');
7
7
  const bytes=await response.arrayBuffer();const hash=[...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(n=>n.toString(16).padStart(2,'0')).join('');
8
8
  if(hash!==receipt.sha256)throw Error('Example integrity check failed. Retry.');
9
9
  const url=URL.createObjectURL(new Blob([bytes],{type:'application/zip'})),link=document.createElement('a');link.href=url;link.download='atmem-ai-complete-example.zip';link.click();setTimeout(()=>URL.revokeObjectURL(url),60000);
@@ -0,0 +1,62 @@
1
+ /* Keep the conversation and the place to reply visibly distinct. */
2
+ .chat { container-type:inline-size; }
3
+ .chat > .chat-body { display:flex;flex:1;flex-direction:column;min-height:0;min-width:0; }
4
+ .chat-history { position:relative;display:flex;flex:1;flex-direction:column;min-height:0;min-width:0;overflow:hidden; }
5
+ .chat-history-label { margin:8px 16px 0;font-size:12px;font-weight:600;color:var(--ink-2); }
6
+ .chat-history .messages { min-height:0;padding-top:10px; }
7
+ .chat-compose-pane { overflow-anchor:none;display:flex;flex-direction:column;flex-shrink:0;min-width:0; }
8
+ .chat-history .chat-jump-latest { bottom:12px;right:16px; }
9
+ .chat .composer { border:2px solid var(--color-brand-text);background:var(--paper);padding:10px;gap:6px; }
10
+ .composer-label { display:block;font-size:13px;font-weight:700;color:var(--ink);line-height:1.4; }
11
+ .chat .composer textarea { box-sizing:border-box;min-height:76px;padding:8px;background:var(--board);border-radius:8px; }
12
+ .chat .composer textarea:focus { outline:2px solid var(--color-brand-text);outline-offset:-2px; }
13
+ .chat .composer-actions .send { display:inline-flex;align-items:center;justify-content:center;gap:5px;width:auto;min-width:70px;padding:0 10px;border-radius:10px;background:var(--color-brand);color:var(--color-brand-ink);font-weight:700; }
14
+ .chat .composer-actions .send:disabled { background:var(--line);color:var(--ink-2); }
15
+ /* A wide chat drawer can use the same two-column conversation/reply layout. */
16
+ @container (min-width:600px) {
17
+ .chat-body { display:grid!important;grid-template-columns:minmax(0,1.2fr) minmax(220px,1fr); }
18
+ .chat-compose-pane { min-height:0;overflow:auto;padding-top:8px;border-left:1px solid var(--line); }
19
+ .chat-compose-pane .composer { order:-1;position:sticky;top:8px;flex-shrink:0;z-index:1; }
20
+ }
21
+ @media (max-width:900px) {
22
+ .app:not(.chat-closed) { height:100dvh;min-height:0;grid-template-rows:auto auto minmax(0,2fr) minmax(300px,3fr);overflow:hidden; }
23
+ .app:not(.chat-closed) .chat { height:auto;min-height:0;overflow:hidden; }
24
+ .chat > .chat-body { display:grid;grid-template-columns:minmax(0,1fr) minmax(148px,.9fr); }
25
+ .chat-compose-pane { min-height:0;overflow:auto;border-left:1px solid var(--line);padding:8px 0 0; }
26
+ .chat-compose-pane .composer { order:-1;position:sticky;top:8px;flex-shrink:0;z-index:1;margin:0 8px 8px;padding:8px; }
27
+ .chat-head { display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;padding:8px 12px; }
28
+ .chat-heading-row { grid-column:1/-1; }
29
+ .chat-head .model { grid-column:1;grid-row:2;justify-self:start;max-width:100%; }
30
+ .chat-head .live-toggle { grid-column:2;grid-row:2;order:0;flex-basis:auto;white-space:nowrap; }
31
+ .chat .composer textarea { min-height:64px;max-height:180px;font-size:16px; }
32
+ .chat .composer-actions { gap:4px; }
33
+ .chat .composer-actions .attach-button,.chat .composer-actions .mic { width:28px;height:32px;padding:3px;flex-shrink:0; }
34
+ .chat .composer-actions .send { min-width:54px;padding:0 7px;font-size:12px; }
35
+ .chat-history-label { margin-left:12px; }
36
+ .board-start { top:50%;max-height:calc(100% - 24px);overflow:auto; }
37
+ .board-start h2 { margin-top:0;font-size:18px; }
38
+ .board-surface:has(.board-start) > .board-components:not([data-expanded=true]) { top:auto;bottom:8px; }
39
+ .chat-history .messages { padding:10px 12px; }
40
+ .chat .skills { flex-wrap:nowrap;overflow-x:auto;flex-shrink:0; }
41
+ .chat .skills > * { flex-shrink:0;white-space:nowrap; }
42
+ .chat-compose-pane .prompts { padding:0 8px 8px; }
43
+ }
44
+ @media (max-width:900px) and (max-height:600px) {
45
+ .app:not(.chat-closed) { grid-template-rows:auto auto minmax(0,1fr) minmax(238px,3fr); }
46
+ .chat .composer textarea { min-height:54px; }
47
+ .chat-head { padding-top:6px;padding-bottom:4px; }
48
+ }
49
+
50
+ @media (max-width:900px) and (min-height:760px) { .chat .composer textarea { min-height:88px; } }
51
+ /* Keep navigation usable when a short window or on-screen keyboard reduces height. */
52
+ @media (max-width:900px) and (max-height:600px) {
53
+ .app:not(.chat-closed) { grid-template-rows:auto auto minmax(0,1fr) minmax(280px,3fr); }
54
+ .unified-journey { flex-direction:row;align-items:center;padding:4px;gap:4px; }
55
+ .unified-journey .stage-tabs { flex:1;min-width:0;width:0;padding:4px; }
56
+ .unified-journey .journey-current { flex:0 1 110px;min-width:0;max-width:110px;padding:0; }
57
+ .unified-journey .journey-current .btn { padding:6px 8px;font-size:11px;min-height:36px; }
58
+ .chat-compose-pane .composer { padding:6px;gap:4px; }
59
+ }
60
+ /* Keep the next action on the same row as the scrolling stage tabs. */
61
+ .unified-journey .journey-current { flex:0 0 auto;max-width:none;margin:0;padding:0; }
62
+ .unified-journey .journey-current .btn { white-space:nowrap;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.2; }
@@ -1,5 +1,12 @@
1
1
  // Backend-owned jobs: no native thread IDs, host paths or model credentials.
2
2
  import {unzipSync} from 'fflate';
3
+ export function engineeringSelectionSupported(session,connection){
4
+ if(!connection?.model||connection.model==='default'||!connection.effort)return false;
5
+ return connection.provider==='codex'||!!(session?.hosted&&connection.provider==='openrouter'&&connection.effort==='medium');
6
+ }
7
+ export function engineeringDeliveryNeedsRefresh(report,job){
8
+ return ['starting','running'].includes((job||report?.session)?.state)||!!report?.checks?.some(check=>['queued','running'].includes(check.state));
9
+ }
3
10
  export async function engineeringCall(session, action, payload, {signal,fetcher=fetch}={}) {
4
11
  if(!session?.token)throw new Error('Connect to the session service first.');
5
12
  const response=await fetcher(`/api/engineering/${action}`,{method:'POST',signal,
@@ -0,0 +1,52 @@
1
+ // Same-origin parent owns Firebase identity; the workspace never receives a token or provider key.
2
+ export const HOSTED=import.meta.env?.VITE_ARCHITECT_HOSTED==='1';
3
+ let initial=null,port=null,sequence=0;const waiting=new Map(),versions=new Map();let changeSequence=0;
4
+ export function hostedRevision(id){return versions.get(id)||0;}
5
+ export function noteHostedChange(projectId){const version=++changeSequence;versions.set(projectId,version);return hostedCall('changed',{projectId,version});}
6
+ export function hostedInitial(){if(!initial)throw Error('Hosted workspace is not connected.');return initial;}
7
+ export function hostedCall(action,payload={},signal){
8
+ if(!port)return Promise.reject(Error('Return to Architect and reopen your project.'));
9
+ if(signal?.aborted)return Promise.reject(new DOMException('Aborted','AbortError'));
10
+ const id=++sequence;
11
+ return new Promise((resolve,reject)=>{
12
+ const timeout=setTimeout(()=>finish(Error('The workspace request timed out. Your draft remains open.')),10*60*1000);
13
+ const abort=()=>{port.postMessage({type:'abort',id});finish(new DOMException('Aborted','AbortError'));};
14
+ function finish(error,value){clearTimeout(timeout);signal?.removeEventListener('abort',abort);waiting.delete(id);error?reject(error):resolve(value);}
15
+ waiting.set(id,finish);signal?.addEventListener('abort',abort,{once:true});port.postMessage({type:'request',id,action,payload});
16
+ });
17
+ }
18
+ export async function connectHostedWorkspace(){
19
+ if(!HOSTED)return;
20
+ if(window.parent===window)throw Error('Open your project from atmem.ai/architect.');
21
+ initial=await new Promise((resolve,reject)=>{
22
+ const timeout=setTimeout(()=>{window.removeEventListener('message',receive);reject(Error('Return to Architect and reopen your project.'));},15000);
23
+ function receive(event){
24
+ if(event.origin!==location.origin||event.source!==parent||event.data?.type!=='atmem.workspace.initialize'||event.ports.length!==1)return;
25
+ clearTimeout(timeout);window.removeEventListener('message',receive);port=event.ports[0];
26
+ port.onmessage=({data})=>{if(data?.type==='response')waiting.get(data.id)?.(data.error?Error(data.error):null,data.value);};port.start();resolve(event.data.initial);
27
+ }
28
+ window.addEventListener('message',receive);window.parent.postMessage({type:'atmem.workspace.ready'},location.origin);
29
+ });
30
+ // Do not leave project drafts, conversations or profile state in a shared browser's local storage.
31
+ const values=new Map([['atmem-architect-theme',initial.theme||'light']]);
32
+ const transient={getItem:key=>values.get(String(key))??null,setItem:(key,value)=>values.set(String(key),String(value)),removeItem:key=>values.delete(String(key)),clear:()=>values.clear(),key:index=>[...values.keys()][index]??null,get length(){return values.size;}};
33
+ Object.defineProperty(window,'localStorage',{value:transient,configurable:true});
34
+ const network=window.fetch.bind(window);
35
+ window.fetch=async(input,options={})=>{
36
+ const url=new URL(typeof input==='string'?input:input.url,location.href);
37
+ if(url.origin!==location.origin||!url.pathname.startsWith('/api/'))return network(input,options);
38
+ let body=null;
39
+ if(options.body){
40
+ if(typeof options.body==='string')body=JSON.parse(options.body);
41
+ else{
42
+ const bytes=new Uint8Array(options.body instanceof Blob?await options.body.arrayBuffer():options.body);
43
+ let binary='';for(let start=0;start<bytes.length;start+=8192)binary+=String.fromCharCode(...bytes.subarray(start,start+8192));
44
+ body={__workspaceBinary:btoa(binary)};
45
+ }
46
+ }
47
+ if(url.pathname==='/api/projects/save'&&body)body.bridgeVersion??=hostedRevision(body.id);
48
+ const result=await hostedCall('fetch',{path:url.pathname,method:options.method||'GET',body},options.signal);
49
+ const content=result.base64?Uint8Array.from(atob(result.base64),character=>character.charCodeAt(0)):result.bytes?new Uint8Array(result.bytes):JSON.stringify(result.body);
50
+ return new Response(content,{status:result.status,headers:result.headers||{'Content-Type':'application/json'}});
51
+ };
52
+ }
@@ -4,8 +4,9 @@
4
4
  .stage-tabs>button[aria-selected=true]{outline:2px solid var(--accent,#be5679);outline-offset:-2px;font-weight:600}
5
5
  .stage-tabs>button.stage-complete{background:#e7f4eb;color:#215d3b;border-color:#a9cdb4}
6
6
  .stage-tabs>button>span{font-size:.85em;border:1px solid currentColor;border-radius:50%;width:22px;height:22px;display:grid;place-items:center}
7
- .unified-journey{display:flex;flex-direction:column;align-items:stretch}
8
- .unified-journey .journey-current{padding:4px 14px;display:flex;gap:12px;align-items:center}
7
+ .unified-journey{display:flex;flex-direction:row;align-items:center}
8
+ .unified-journey .stage-tabs{width:auto;flex:0 1 auto;min-width:0;padding:2px}
9
+ .unified-journey .journey-current{margin:0;padding:0;display:flex;flex:0 0 auto;gap:0;align-items:center}
9
10
  .unified-journey .journey-current p{margin:4px 0;flex:1}
10
11
  .project-rename{display:flex;align-items:center;min-width:0}.project-rename>svg{opacity:0}.project-rename:hover>svg,.project-rename:focus-within>svg{opacity:1}
11
12
  .bar .projects-btn{display:inline-flex;align-items:center;gap:7px;width:auto;min-width:100px;padding:7px 10px}
@@ -109,3 +110,5 @@
109
110
  .star-prompt{width:min(460px,calc(100vw - 32px));max-width:460px;border:1px solid var(--line);border-radius:16px;padding:22px;background:var(--paper);color:var(--ink);box-shadow:0 18px 60px #0003}.star-prompt::backdrop{background:#0005}.star-prompt header,.star-prompt h2,.star-prompt-actions{display:flex;align-items:center;gap:10px}.star-prompt header{justify-content:space-between}.star-prompt h2{font-size:1.2rem}.star-prompt h2>svg{color:var(--accent);fill:var(--accent-soft)}.star-prompt p{margin:18px 0;line-height:1.5}.star-prompt-actions>a{flex:1;text-decoration:none}.star-prompt-actions>button{min-width:90px}
110
111
 
111
112
  .check-run-action{display:inline-flex;margin-left:12px;flex-shrink:0}.check-run-action>button:disabled{pointer-events:none}.check-run-action:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
113
+
114
+ @media(max-width:650px){.unified-journey .stage-tabs{flex:1;min-width:0;width:0}.unified-journey .journey-current{max-width:120px}.unified-journey .journey-current>.btn{white-space:normal;padding:7px 9px;font-size:12px;min-height:36px}}
@@ -99,7 +99,7 @@ export async function discoverLocalAgent() {
99
99
  });
100
100
  if (!response.ok) throw Error("Local sidecar unavailable");
101
101
  const session = await response.json();
102
- if (!session.token || session.status !== "local-sidecar")
102
+ if (!session.token || !["local-sidecar","hosted-workspace"].includes(session.status))
103
103
  throw Error("Invalid local sidecar response");
104
104
  return session;
105
105
  }
package/src/main.jsx CHANGED
@@ -1,3 +1,4 @@
1
+ import {HOSTED,connectHostedWorkspace,hostedInitial,hostedCall,hostedRevision,noteHostedChange} from './hosted-workspace.mjs';
1
2
  import {readyExample,isReadyExample} from './ready-example.mjs';
2
3
  import {ReadyExampleDownload} from './ReadyExample.jsx';
3
4
  import {StarPrompt,STAR_URL} from './StarPrompt.jsx';
@@ -171,6 +172,7 @@ import { zipSync, strToU8 } from "fflate";
171
172
  const STORAGE = "atforma-workspace-v3";
172
173
  // A new project starts empty: the user describes what they want first.
173
174
  import './onboarding.css';
175
+ import './chat-composer.css';
174
176
  const emptyProject = () => ({ id: crypto.randomUUID(), version: 1, revision: 1, name: "New project", provider: "tools", requirements: "", nodes: [], edges: [] });
175
177
  const ALL_CATALOG = [...CATALOG, ...UPSTREAM_CATALOG];
176
178
  const KIND_ICON = {
@@ -257,6 +259,7 @@ function Mark({ size = 20 }) {
257
259
  // Which architect the user chose; absent on first visit, so we ask before anything else.
258
260
  const CONNECTION = "atforma-connection-v1";
259
261
  function readConnection() {
262
+ if(HOSTED)return hostedInitial().connection;
260
263
  try {
261
264
  const c = JSON.parse(localStorage.getItem(CONNECTION) || "null");
262
265
  // Older builds stored "offline"; the architect now always needs a real model.
@@ -273,6 +276,7 @@ function saveConnection(c) {
273
276
  } catch {}
274
277
  }
275
278
  function readSaved() {
279
+ if(HOSTED)return withDerived(fromProjectFile(hostedInitial().file).project);
276
280
  try {
277
281
  const raw = localStorage.getItem(STORAGE);
278
282
  const p = validateImport(raw ? JSON.parse(raw) : emptyProject());
@@ -665,14 +669,14 @@ function savedTheme() {
665
669
  return "light";
666
670
  }
667
671
  }
668
- document.documentElement.dataset.theme = savedTheme();
672
+
669
673
 
670
674
  function readChatView(id){try{const value=JSON.parse(localStorage.getItem('atmem-chat-view:'+id)||'null');return value&&Number.isSafeInteger(value.start)&&value.start>=0&&Array.isArray(value.turnIds)?value:{start:0,turnIds:[]};}catch{return {start:0,turnIds:[]};}}
671
675
  const pricingSignature=p=>JSON.stringify([p.id,p.estimate?.projectSize||'small',p.estimate?.usage,p.facts,p.decisions?.filter(d=>d.status==='Accepted'&&['cloud.region','data.country'].includes(d.subject)),[...storageNodes(p),...lambdaNodes(p),...googleNodes(p)].map(n=>[n.id,n.data.provider,n.data.service,n.data.config])]);
672
676
  function App() {
673
677
  const [starOffer,setStarOffer]=useState(0);
674
678
  const [introOpen,setIntroOpen]=useState(false),[voiceOpenRequest,setVoiceOpenRequest]=useState(0);
675
- const [project, setProject] = useState(()=>new URL(location.href).searchParams.get('example')==='1'&&!projectIdFromUrl(location.href)?readyExample():readSaved());
679
+ const [project, setProject] = useState(()=>!HOSTED&&new URL(location.href).searchParams.get('example')==='1'&&!projectIdFromUrl(location.href)?readyExample():readSaved());
676
680
  const projectRef = useRef(project);
677
681
  projectRef.current = project;
678
682
  const [initialCanvasView] = useState(()=>readCanvasView(project));
@@ -917,16 +921,19 @@ function App() {
917
921
  return () => { clearTimeout(zoom); clearTimeout(clear); };
918
922
  }, [attention, fitView]);
919
923
 
924
+ function showConnections(){if(HOSTED){void hostedCall('account').catch(error=>notify(error.message));return;}setModal('connections');}
920
925
  const refreshSession = useCallback(async () => {
921
926
  try {
922
927
  const next = await discoverLocalAgent();
923
928
  setSession(next);
929
+ if(HOSTED&&next.defaultConnection)setConnection(current=>current?.model===next.defaultConnection.model&&current?.effort===next.defaultConnection.effort?current:next.defaultConnection);
924
930
  return next;
925
931
  } catch {
926
932
  setSession(null);
927
933
  return null;
928
934
  }
929
935
  }, []);
936
+ useEffect(()=>{if(HOSTED&&session?.token)void hostedCall('ready').catch(()=>{});},[session?.token]);
930
937
  useEffect(() => {
931
938
  refreshSession();
932
939
  const timer=setInterval(refreshSession,30000);
@@ -1216,7 +1223,11 @@ function App() {
1216
1223
  const raw = JSON.parse(content);
1217
1224
  if (raw.format || raw.version === 1) {
1218
1225
  const { project: opened, migrated, from } = parseProjectFile(content);
1219
- loadDesign(fileImportAsCopy.current?copyProject(opened,crypto.randomUUID()):opened, { own: true });
1226
+ if(HOSTED){
1227
+ await saveEngineeringProject(projectRef.current.id);
1228
+ const imported=await hostedCall('import-project',{file:toProjectFile(opened)});
1229
+ loadDesign(fromProjectFile(imported.file).project,{own:true});
1230
+ }else loadDesign(fileImportAsCopy.current?copyProject(opened,crypto.randomUUID()):opened, { own: true });
1220
1231
  notify(fileImportAsCopy.current?"New project created from the saved design. The original is preserved.":migrated ? `Project opened and upgraded from ${from}.` : "Project opened.");
1221
1232
  } else {
1222
1233
  loadDesign(importUpstreamProject(raw));
@@ -1649,13 +1660,15 @@ function App() {
1649
1660
  updateEngagement({ ...skipped, lastAsked: next });
1650
1661
  setMessages((m) => [...m, { role: "assistant", text: next ? `No problem, we'll come back to it.\n\n${next.question}\n${next.beginner}` : "That's everything I need for now." }]);
1651
1662
  }
1652
- // Autosave to the helper (your account on this computer), a moment after each change.
1663
+ useEffect(()=>{if(HOSTED)void noteHostedChange(project.id).catch(()=>{});},[project,messages]);
1664
+ // Autosave through the selected local or hosted session service.
1653
1665
  const binned = useRef(new Set());
1654
1666
  useEffect(() => {
1655
- if (!session || binned.current.has(project.id) || (!project.nodes.length && !project.brief?.application?.trim() && messages.length < 2)) return;
1667
+ if (!session || binned.current.has(project.id) || (!HOSTED && !project.nodes.length && !project.brief?.application?.trim() && messages.length < 2)) return;
1668
+ const bridgeVersion=HOSTED?hostedRevision(project.id):undefined;
1656
1669
  const t = setTimeout(async () => {
1657
1670
  try {
1658
- const saved=await projectStore(session, "save", { id: project.id, file: toProjectFile(withConversation(project, messages)) });
1671
+ const saved=await projectStore(session, "save", { id: project.id, file: toProjectFile(withConversation(project, messages)),...(HOSTED?{bridgeVersion}:{}) });
1659
1672
  if(saved.memory)setMemorySync({projectId:project.id,...saved.memory});
1660
1673
  } catch (error) { setMemorySync(current=>({...((current?.projectId===project.id)?current:{}),projectId:project.id,status:'sync_failed'}));notify(`Project conversation could not be saved: ${error.message}`); }
1661
1674
  }, 800);
@@ -1711,6 +1724,7 @@ function App() {
1711
1724
  }
1712
1725
  async function openSaved(id) {
1713
1726
  try {
1727
+ if(HOSTED)await saveEngineeringProject(projectRef.current.id);
1714
1728
  const { project: opened } = fromProjectFile(await projectStore(session, "get", { id }));
1715
1729
  switchTo(opened);
1716
1730
  setModal(null);
@@ -1739,20 +1753,30 @@ function App() {
1739
1753
  })();
1740
1754
  return () => { active = false; };
1741
1755
  }, [session?.token]);
1742
- function newProject() { if(!busy&&!agentBusy&&!intake.length)setModal("start-over"); }
1756
+ async function newProject() {
1757
+ if(HOSTED){
1758
+ if(agentBusy||busy||intake.length)return;
1759
+ try{await saveEngineeringProject(projectRef.current.id);const {file}=await hostedCall('new-project');switchTo(fromProjectFile(file).project);setModal(null);}catch(error){notify(error.message);}
1760
+ return;
1761
+ }
1762
+ if(!busy&&!agentBusy&&!intake.length)setModal("start-over");
1763
+ }
1743
1764
  function createOwnProject(){
1765
+ if(HOSTED){void newProject();return;}
1744
1766
  switchTo(validateImport(emptyProject()));setModal(connection?null:'connections');
1745
1767
  requestAnimationFrame(()=>document.querySelector('input[aria-label="Project name"]')?.focus());
1746
1768
  }
1747
- function openReadyExample(){
1769
+ async function openReadyExample(){
1748
1770
  if(busy||agentBusy||intake.length)return;
1749
- const example=readyExample();example.id=crypto.randomUUID();switchTo(example);setModal(null);
1771
+ const example=readyExample();example.id=crypto.randomUUID();
1772
+ if(HOSTED){try{await saveEngineeringProject(projectRef.current.id);const {file}=await hostedCall('import-project',{file:toProjectFile(example)});switchTo(fromProjectFile(file).project);setModal(null);}catch(error){notify(error.message);}return;}
1773
+ switchTo(example);setModal(null);
1750
1774
  }
1751
1775
  async function binProject(id) {
1752
1776
  try {
1753
1777
  binned.current.add(id);
1754
1778
  await projectStore(session, "trash", { id });
1755
- if (id === projectRef.current.id) switchTo(validateImport(emptyProject()));
1779
+ if(id===projectRef.current.id){if(HOSTED){const {file}=await hostedCall('new-project');switchTo(fromProjectFile(file).project);}else switchTo(validateImport(emptyProject()));}
1756
1780
  await refreshProjects();
1757
1781
  notify("Moved to the bin. Restore it from Projects any time.");
1758
1782
  } catch (e) {
@@ -1868,7 +1892,7 @@ function App() {
1868
1892
  </button>
1869
1893
  <nav className="crumbs" aria-label="Level"><span className="workspace-label">PROJECT</span>
1870
1894
  <button className="projects-btn" aria-label="Projects" title="Your projects" onClick={() => (refreshProjects(), setModal("projects"))}>
1871
- <FolderOpen size={22} /> Projects
1895
+ <FolderOpen size={22} /><span>Projects</span>
1872
1896
  </button>
1873
1897
  <label className="project-rename"><input
1874
1898
  className="project-name"
@@ -1879,7 +1903,8 @@ function App() {
1879
1903
  /><PencilRuler size={14} aria-hidden="true"/></label>
1880
1904
  </nav>
1881
1905
  <div className="bar-actions">
1882
- {isReadyExample(project)&&<><ReadyExampleDownload compact/><button className="btn" onClick={newProject} disabled={busy||agentBusy}>Create new project</button></>}
1906
+ {HOSTED&&<><button className="btn" onClick={newProject} disabled={busy||agentBusy}><Plus size={16}/>New project</button><button className="btn" onClick={()=>hostedCall('account')}>Account</button></>}
1907
+ {isReadyExample(project)&&<><ReadyExampleDownload compact/>{!HOSTED&&<button className="btn" onClick={newProject} disabled={busy||agentBusy}>Create new project</button>}</>}
1883
1908
  {storageError && <span className="warn-text">Not saved</span>}
1884
1909
  <button
1885
1910
  className="icon"
@@ -1915,7 +1940,7 @@ function App() {
1915
1940
  <hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><BrainActivity size={18} active={false}/>AtMem memory</button>
1916
1941
  <button role="menuitem" onClick={() => setModal("start-over")} disabled={busy || agentBusy}><RotateCcw size={16}/>Start over</button>
1917
1942
  <hr />
1918
- <button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>AI providers</button>
1943
+ {!HOSTED&&<button role="menuitem" onClick={() => showConnections()}><Waypoints size={16}/>AI providers</button>}
1919
1944
  <button role="menuitem" onClick={() => setModal("licenses")}><ShieldCheck size={16}/>Licences</button>
1920
1945
  <a role="menuitem" href={STAR_URL} target="_blank" rel="noopener noreferrer">Star on GitHub ↗</a>
1921
1946
  </div>
@@ -2197,7 +2222,7 @@ function App() {
2197
2222
  <section className="review-costs" aria-label="Project costs">
2198
2223
  {["review","estimate"].includes(mode) && <EstimateView project={project} result={costs} onChange={updateEngagement} busy={busy||agentBusy||!!pending} onEstimate={refreshEstimate} onChooseRegion={()=>{visitJourney('cloud');requestAnimationFrame(()=>{const input=document.querySelector('.location-region-form input');input?.focus();input?.scrollIntoView({block:'center',behavior:'instant'});});}} pricing={pricingState} onNext={()=>{const section=document.querySelector('.review-generation');const target=section?.querySelector('button')||section;if(target){target.setAttribute('tabindex','-1');target.focus({preventScroll:true});target.scrollIntoView({block:'center'});}}} />}
2199
2224
  </section>
2200
- {mode==='review'&&session?.engineeringEnabled&&<section className="review-generation" aria-label="Generation and verification"><EngineeringPanel key={project.id} project={project} onBusyChange={setGenerationPending} onWorkModelChange={changeArchitectureWork} onIntentContext={syncEngineeringIntent} projectId={project.id} session={session} connection={connection} contextBusy={busy||agentBusy||!!pending} saveProject={()=>saveEngineeringProject(project.id)} buildRequest={buildRequest} onBuildRequestHandled={id=>setBuildRequest(current=>current?.id===id?null:current)} onConnections={()=>setModal('connections')}/></section>}
2225
+ {mode==='review'&&session?.engineeringEnabled&&<section className="review-generation" aria-label="Generation and verification"><EngineeringPanel key={project.id} project={project} onBusyChange={setGenerationPending} onWorkModelChange={changeArchitectureWork} onIntentContext={syncEngineeringIntent} projectId={project.id} session={session} connection={connection} contextBusy={busy||agentBusy||!!pending} saveProject={()=>saveEngineeringProject(project.id)} buildRequest={buildRequest} onBuildRequestHandled={id=>setBuildRequest(current=>current?.id===id?null:current)} onConnections={()=>showConnections()}/></section>}
2201
2226
  {mode==='review'&&!session?.engineeringEnabled&&<p>Generation and local checks require the engineering service. Your design is retained; Delivery can package the design and remaining tasks.</p>}
2202
2227
  {mode==='review'&&<ProjectChecks generationPending={generationPending} receipt={checkReceipt} project={project} environment={environment} session={session} onReceipt={setCheckReceipt} onBusy={setChecking}/>}
2203
2228
  {mode==='review'&&<footer className="review-finish"><div><strong>3. Package and download</strong><p>Explore all files and download one project ZIP in Delivery.</p></div><button className="btn primary" onClick={()=>{const next=acknowledgeStage(projectRef.current,'review');updateEngagement(costs.expected!==null&&!costs.mixedCurrencies?{...next,extensions:{...next.extensions,journey:{...next.extensions.journey,estimate:journey(next).keys.estimate}}}:next);visitJourney('delivery');}}>Complete →</button></footer>}
@@ -2208,7 +2233,7 @@ function App() {
2208
2233
 
2209
2234
  {onBoard&&emulationOpen&&<div className="emulation-dock"><EmulationPanel project={project} session={session} emulation={emulation} onDiscuss={discussEmulation} onOpenComponent={id=>{reviewBlock(id);setEmulationOpen(false);}} discussionBusy={busy||agentBusy||!!pending} onClose={()=>setEmulationOpen(false)} onBuild={session?.engineeringEnabled?()=>{setEmulationOpen(false);buildCurrentDesign('test');}:undefined}/></div>}
2210
2235
 
2211
- {mode === "design" && designView === "outputs" && <OutputsView onNavigate={async target=>{const [stage,selector]={requirements:['scope','.requirements-document'],location:['cloud','section[aria-label="Country and region"]'],cost:['review','.review-costs'],implementation:['review','.review-generation'],checks:['review','.project-checks']}[target];visitJourney(stage);await new Promise((resolve,reject)=>{const started=performance.now();function focus(){const element=document.querySelector(selector);if(element&&element.getClientRects().length){element.setAttribute('tabindex','-1');element.focus({preventScroll:true});element.scrollIntoView({block:'start',behavior:'instant'});resolve();}else if(performance.now()-started<5000)requestAnimationFrame(focus);else reject(Error('Section is unavailable. Use the stage tabs to continue.'));}requestAnimationFrame(focus);});}} checkReceipt={checkReceipt} onDownloaded={()=>{updateEngagement(acknowledgeStage(projectRef.current,'delivery'));setStarOffer(value=>value+1);}} onReview={()=>visitJourney('review')} onSave={expected=>saveEngineeringProject(project.id,expected)} project={project} run={run} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDownload={downloadBundle} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
2236
+ {mode === "design" && designView === "outputs" && <OutputsView workStatus={engineeringStatus} onNavigate={async target=>{const [stage,selector]={requirements:['scope','.requirements-document'],location:['cloud','section[aria-label="Country and region"]'],cost:['review','.review-costs'],implementation:['review','.review-generation'],checks:['review','.project-checks']}[target];visitJourney(stage);await new Promise((resolve,reject)=>{const started=performance.now();function focus(){const element=document.querySelector(selector);if(element&&element.getClientRects().length){element.setAttribute('tabindex','-1');element.focus({preventScroll:true});element.scrollIntoView({block:'start',behavior:'instant'});resolve();}else if(performance.now()-started<5000)requestAnimationFrame(focus);else reject(Error('Section is unavailable. Use the stage tabs to continue.'));}requestAnimationFrame(focus);});}} checkReceipt={checkReceipt} onDownloaded={()=>{updateEngagement(acknowledgeStage(projectRef.current,'delivery'));setStarOffer(value=>value+1);}} onReview={()=>visitJourney('review')} onSave={expected=>saveEngineeringProject(project.id,expected)} project={project} run={run} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDownload={downloadBundle} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
2212
2237
  {mode === "design" && designView === "terraform" && <TerraformView project={project} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDelivery={()=>visitJourney("delivery")} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
2213
2238
  <div className="choices-workspace" hidden={!(mode==="design"&&["options","decisions","location"].includes(designView))}>
2214
2239
  {mode === "design" && ["options","decisions"].includes(designView) && (
@@ -2241,10 +2266,11 @@ function App() {
2241
2266
  />
2242
2267
  Draw as we talk
2243
2268
  </label>
2244
- <button className="model" onClick={() => setModal("connections")} title="AI providers">
2269
+ {HOSTED?<span className="model"><span className={`led ${connection&&session?'on':''}`}/>{connection?.model?.split('/').at(-1)} · Medium</span>:<>
2270
+ <button className="model" onClick={() => showConnections()} title="AI providers">
2245
2271
  <span className={`led ${connection && session ? "on" : ""}`} />
2246
2272
  {connection ? `${connection.model}${connection.effort ? ` · ${EFFORT_LABEL[connection.effort] || connection.effort}` : ""}` : "Connect a model"}
2247
- </button>
2273
+ </button></>}
2248
2274
  <span className="chat-top-utilities">
2249
2275
  {hasIntroduction&&<button type="button" aria-expanded={introOpen} onClick={()=>setIntroOpen(value=>!value)}>{introOpen?'▾':'▸'} Introduction</button>}
2250
2276
  {session&&<button type="button" className="icon" aria-label="Voice settings" title="Voice settings" onClick={()=>setVoiceOpenRequest(value=>value+1)}><Volume2 size={15}/></button>}
@@ -2292,6 +2318,8 @@ function App() {
2292
2318
  ))}
2293
2319
  </ul>
2294
2320
  )}
2321
+ <div className="chat-body">
2322
+ <section className="chat-history" aria-label="Chat history"><h2 className="chat-history-label">Chat history</h2>
2295
2323
  <div className="messages" ref={chatScroll.viewport}>
2296
2324
  <div className="message-list" ref={chatScroll.content}>
2297
2325
  {architectureChatEntries(project,messages).filter(entry=>entry.kind==='message'?entry.index>=chatView.start:entry.kind==='activity'?!chatView.turnIds.includes(entry.turn.id):entry.kind==='legacy-activity'?entry.entries.some(item=>!chatView.turnIds.includes(item.turn.id)):true).map(entry=>{
@@ -2306,7 +2334,7 @@ function App() {
2306
2334
  {(m.workState==='queued'||m.workState==='processing'&&intake.some(q=>q.id===m.requestId))&&<small className="message-work-state">Queued for the next update <button className="link" onClick={()=>{updateIntake(intakeRef.current.filter(q=>q.id!==m.requestId));setRequestState([m.requestId],'cancelled');}}>Remove from queue</button><button className="link" onClick={()=>{if(input||attachments.length){notify('Your current draft is preserved. Send or clear it before editing a queued update.');return;}const queued=intakeRef.current.find(q=>q.id===m.requestId);if(!queued)return;setInput(queued.text);setAttachments(queued.attachments);updateIntake(intakeRef.current.filter(q=>q.id!==m.requestId));setRequestState([m.requestId],'cancelled');}}>Edit update</button>{m.attachments?.some(a=>!a.data)&&<button className="link" onClick={()=>{updateIntake(intakeRef.current.map(q=>q.id===m.requestId?{...q,attachments:[]}:q));setIntakePaused(false);}}>Continue with text only</button>}</small>}
2307
2335
  {!!m.attachments?.length&&<div className="sent-attachments">{m.attachments.map(a=><figure key={a.id}>{a.data&&a.mime.startsWith('image/')&&<img src={`data:${a.mime};base64,${a.data}`} alt={a.name}/>}<figcaption><Paperclip size={12}/>{a.name}{a.originalNotSaved && <small>Original file not saved; attach it again to review its contents.</small>}</figcaption></figure>)}</div>}
2308
2336
  {m.fix && (
2309
- <button className="btn fix" onClick={() => setModal("connections")}>
2337
+ <button className="btn fix" onClick={() => showConnections()}>
2310
2338
  Fix connection
2311
2339
  </button>
2312
2340
  )}
@@ -2349,6 +2377,8 @@ function App() {
2349
2377
  </div>
2350
2378
  </div>
2351
2379
  {chatScroll.away&&<button className="chat-jump-latest" aria-label="Jump to latest message" onClick={chatScroll.followLatest}>↓</button>}
2380
+ </section>
2381
+ <div className="chat-compose-pane">
2352
2382
  {!!intake.filter(q=>q.projectId===project.id).length&&<div className="intake-status" role="status">{intake.filter(q=>q.projectId===project.id).length} update(s) {intakePaused?'saved · paused':'queued · combining next'}{intakeIssue&&<span> {intakeIssue}</span>}{intakePaused&&<button className="link" onClick={()=>setIntakePaused(false)}>Resume</button>}</div>}
2353
2383
  <div className="prompts">
2354
2384
  {mode === "design" && project.workflow && !project.nodes.length && !pending && (
@@ -2371,11 +2401,12 @@ function App() {
2371
2401
  void postMessage();
2372
2402
  }}
2373
2403
  >
2374
- <textarea
2404
+ <label className="composer-label" htmlFor="architect-message">Your message</label>
2405
+ <textarea id="architect-message"
2375
2406
  onPaste={e=>{const files=[...e.clipboardData.files];if(files.length){e.preventDefault();void attachFiles(files);}}}
2376
2407
  ref={messageInput}
2377
2408
  aria-label="Your message"
2378
- placeholder={brief(project) ? "Reply in your own words…" : "Describe what you want to build…"}
2409
+ placeholder="Type here…"
2379
2410
  rows={2}
2380
2411
  value={input}
2381
2412
  onChange={(e) => { stopVoice(); setInput(e.target.value); }}
@@ -2401,11 +2432,11 @@ function App() {
2401
2432
  {voiceActive ? <Square size={13} /> : <Mic size={15} />}
2402
2433
  </button>
2403
2434
  <button type="submit" className="send" aria-label="Send message" disabled={(!input.trim() && !voiceActive && !attachments.length) || busy || readingAttachments || voiceState === "transcribing"}>
2404
- <ArrowUp size={15} />
2435
+ <ArrowUp size={15} /><span>Send</span>
2405
2436
  </button>
2406
2437
  </div>
2407
2438
  </form>
2408
- {session&&<OpenAIVoicePanel hideTrigger openRequest={voiceOpenRequest} key={project.id} session={session} connection={connection} transportRef={openaiVoice} onConnections={()=>setModal('connections')} onStart={()=>{voice.current?.cancel();setVoiceError('');}} onTranscript={(text,automatic)=>{
2439
+ {session&&<OpenAIVoicePanel hideTrigger openRequest={voiceOpenRequest} key={project.id} session={session} connection={connection} transportRef={openaiVoice} onConnections={()=>showConnections()} onStart={()=>{voice.current?.cancel();setVoiceError('');}} onTranscript={(text,automatic)=>{
2409
2440
  if(automatic&&!input.trim()&&!attachments.length){setInput(text);void send(text);}
2410
2441
  else setInput(current=>current+(current.trim()?' ':'')+text);
2411
2442
  }}/>}
@@ -2413,10 +2444,12 @@ function App() {
2413
2444
  {!voiceError&&["starting","transcribing"].includes(voiceState)&&<BrainActivity size={22}/>}
2414
2445
  {voiceError || ({ starting: "Opening microphone…", listening: "Dictating locally — review your words, then press Send.", transcribing: "Finishing your transcript…" }[voiceState] || "")}
2415
2446
  </div>
2447
+ </div>
2448
+ </div>
2416
2449
  </aside>
2417
2450
  )}
2418
2451
 
2419
- <input type="file" id="requirements-file-input" ref={requirementsInput} accept={ATTACHMENT_ACCEPT} multiple hidden onChange={async e=>{const files=[...e.target.files];e.target.value='';if(!files.length||readingAttachments)return;const uploadOwner=projectRef.current.id;setReadingAttachments(true);setAttachmentError('');try{const added=await readAttachments(files,attachments);if(projectRef.current.id!==uploadOwner)return;const all=[...attachments,...added];setAttachments(all);setChatOpen(true);setInput('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.');if(connection&&session){setReadingAttachments(false);void send('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.','chat',null,null,{attachments:all});}else{setModal('connections');}}catch(error){setAttachmentError(error.message);}finally{setReadingAttachments(false);}}}/>
2452
+ <input type="file" id="requirements-file-input" ref={requirementsInput} accept={ATTACHMENT_ACCEPT} multiple hidden onChange={async e=>{const files=[...e.target.files];e.target.value='';if(!files.length||readingAttachments)return;const uploadOwner=projectRef.current.id;setReadingAttachments(true);setAttachmentError('');try{const added=await readAttachments(files,attachments);if(projectRef.current.id!==uploadOwner)return;const all=[...attachments,...added];setAttachments(all);setChatOpen(true);setInput('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.');if(connection&&session){setReadingAttachments(false);void send('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.','chat',null,null,{attachments:all});}else{showConnections();}}catch(error){setAttachmentError(error.message);}finally{setReadingAttachments(false);}}}/>
2420
2453
  <input id="project-file-input" type="file" ref={fileRef} accept="application/json,.json" hidden onChange={importProject} />
2421
2454
  {toast && (
2422
2455
  <div className="toast" role="status">
@@ -2494,7 +2527,7 @@ function App() {
2494
2527
  />
2495
2528
  ) : modal === "licenses" ? (
2496
2529
  <>
2497
- <h2>AtMem</h2><a href="/licenses/atmem-APACHE-2.0.txt" target="_blank" rel="noreferrer">Apache 2.0</a>
2530
+ <h2>AtMem</h2><a href={(HOSTED?'/architect/workspace/':'/')+'licenses/atmem-APACHE-2.0.txt'} target="_blank" rel="noreferrer">Apache 2.0</a>
2498
2531
  </>
2499
2532
  ) : (
2500
2533
  <>
@@ -2509,8 +2542,17 @@ function App() {
2509
2542
  );
2510
2543
  }
2511
2544
 
2512
- createRoot(document.getElementById("root")).render(
2545
+ async function mountArchitect(){
2546
+ await connectHostedWorkspace();
2547
+ document.documentElement.dataset.theme=savedTheme();
2548
+ createRoot(document.getElementById("root")).render(
2513
2549
  <ReactFlowProvider>
2514
2550
  <App />
2515
2551
  </ReactFlowProvider>,
2516
2552
  );
2553
+ }
2554
+ void mountArchitect().catch(error=>{
2555
+ const root=document.getElementById('root');root.replaceChildren();
2556
+ const message=document.createElement('p');message.textContent=error.message||'The workspace could not open.';
2557
+ const link=document.createElement('a');link.href='/architect';link.textContent='Return to Architect';root.append(message,link);
2558
+ });
@@ -8,3 +8,17 @@
8
8
  @media(max-width:900px){.app.idle-chat{grid-template-rows:auto auto minmax(100px,1fr) auto}.app>.chat.idle{height:auto;min-height:0}}
9
9
  .ready-reference{margin:20px 0}.ready-reference>p{margin:8px 0}.ready-reference li{display:flex;align-items:center;gap:10px;padding:8px 0}.ready-reference li>span:nth-child(2){flex:1}.ready-reference details{margin-top:10px}.ready-reference details p{margin:8px 0}
10
10
  .bar-actions .ready-download{font-size:12px;padding:6px 10px}.toolbar .reset-board{font-size:12px;padding:6px 9px;gap:5px}
11
+
12
+ /* Keep project tools in one slim row; the diagram owns the remaining height. */
13
+ .app>.bar{flex-wrap:nowrap;gap:6px;padding:4px 8px;min-height:40px}
14
+ .app>.bar>.brand{padding:2px;flex-shrink:0}
15
+ .app>.bar>.crumbs{order:0;flex:1 1 auto;min-width:0;flex-wrap:nowrap;gap:4px!important}
16
+ .app>.bar .workspace-label{display:none}
17
+ .app>.bar .project-rename{flex:1;min-width:0}
18
+ .app>.bar .project-name{font-size:13px;padding:4px 6px;height:30px;min-width:0}
19
+ .app>.bar .projects-btn{padding:4px 6px;min-width:0;min-height:30px;font-size:12px}
20
+ .app>.bar>.bar-actions{margin-left:auto;padding:0;border:0;gap:4px;flex-shrink:0}
21
+ .app>.bar .bar-actions>.btn{padding:5px 8px;font-size:12px;min-height:30px;gap:4px}
22
+ .app>.bar .bar-actions>.icon{width:30px;height:30px;min-height:30px;padding:5px}
23
+ @media(max-width:900px){.app>.bar .bar-actions>.ready-download{display:none}}
24
+ @media(max-width:650px){.app>.bar .projects-btn>span{display:none}.app>.bar>.bar-actions{gap:2px}.app>.bar .bar-actions>.btn{font-size:11px;padding:5px 6px}.app>.bar .bar-actions>.icon{width:26px}.app>.bar .project-name{font-size:12px;padding:3px;}.app>.bar .project-rename>svg{display:none}}