@atmem/architect 0.2.0-alpha.4.1 → 0.2.0

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 (55) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +19 -21
  4. package/dist/assets/index-BTLHaa3J.css +1 -0
  5. package/dist/assets/{index-qtCM2D7O.js → index-CQ2C1gKF.js} +19206 -19139
  6. package/dist/assets/{index.es-C6C0sneB.js → index.es-DTyqIdaO.js} +1 -1
  7. package/dist/assets/{visual-export-DjbjgQbA.js → visual-export-Ox0j5xj5.js} +3 -3
  8. package/dist/index.html +2 -2
  9. package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
  10. package/package.json +2 -2
  11. package/public/licenses/atmem-APACHE-2.0.txt +202 -0
  12. package/server/app.py +1 -1
  13. package/server/codex_app_server.py +5 -5
  14. package/server/codex_worker_policy.py +27 -4
  15. package/server/engineering_artifacts.py +7 -1
  16. package/server/engineering_handoff.py +9 -0
  17. package/server/engineering_routes.py +16 -2
  18. package/server/local_providers.py +7 -3
  19. package/server/memory_context.py +58 -0
  20. package/server/memory_routes.py +67 -8
  21. package/server/memory_service.py +37 -4
  22. package/server/memory_worker.py +72 -9
  23. package/server/orca_handoff.py +90 -0
  24. package/server/pricing.py +106 -0
  25. package/server/prompts.py +13 -0
  26. package/server/windows_snapshot.py +119 -0
  27. package/src/ArchitectureWorkPanel.jsx +0 -7
  28. package/src/ArtifactChecks.jsx +2 -2
  29. package/src/ChatMessage.jsx +12 -2
  30. package/src/EngagementViews.jsx +18 -16
  31. package/src/EngineeringPanel.jsx +27 -27
  32. package/src/FileTree.jsx +7 -0
  33. package/src/FinalChecklist.jsx +15 -0
  34. package/src/HelpNote.jsx +3 -0
  35. package/src/ImplementationDelivery.jsx +52 -16
  36. package/src/JourneyViews.jsx +19 -16
  37. package/src/LocationChoices.jsx +12 -0
  38. package/src/MemoryPanel.jsx +43 -31
  39. package/src/OpenAIVoicePanel.jsx +15 -14
  40. package/src/OutputsView.jsx +21 -15
  41. package/src/ProjectChecks.jsx +44 -0
  42. package/src/WorkbenchDialogs.jsx +1 -1
  43. package/src/conversation.mjs +1 -0
  44. package/src/delivery.mjs +1 -1
  45. package/src/estimate.mjs +5 -3
  46. package/src/final-package.mjs +34 -0
  47. package/src/journey-workspace.css +34 -0
  48. package/src/journey.mjs +12 -2
  49. package/src/main.jsx +119 -96
  50. package/src/output-bundle.mjs +4 -2
  51. package/src/pricing.mjs +56 -2
  52. package/src/useChatScroll.jsx +5 -4
  53. package/src/useEngineeringStatus.mjs +17 -0
  54. package/src/validation-receipt.mjs +1 -1
  55. package/dist/assets/index-B97MkVMs.css +0 -1
package/src/journey.mjs CHANGED
@@ -20,7 +20,7 @@ export function completedStages(p){
20
20
  return {board:p.nodes.some(n=>n.data.kind!=='note'),scope:saved.scope===keys.scope,
21
21
  cloud:cloudMapping(p).ready&&saved.architecture===keys.architecture,
22
22
  review:saved.review===keys.architecture&&saved.estimate===keys.estimate,
23
- delivery:false}; // A visit/download alone never proves successful delivery.
23
+ delivery:saved.delivery===keys.delivery}; // Delivery means the exact package was downloaded, not deployed.
24
24
  }
25
25
  export function journeyAction(guidance,section='board'){
26
26
  if(section===journeySectionId(guidance.id)&&guidance.checkpoint&&section!=='board')return {kind:'continue',label:guidance.continueLabel};
@@ -42,7 +42,7 @@ const snapshot=value=>{const text=JSON.stringify(value);let a=2166136261,b=5381;
42
42
  export function journeyKeys(p){
43
43
  const scope=snapshot([p.brief?.application||p.requirements||'',(p.needs||[]).filter(r=>r.source==='user').map(r=>[r.id,r.statement,r.measure]).sort()]);
44
44
  const architecture=snapshot([scope,p.nodes.filter(n=>n.data.kind!=='note').map(n=>[n.id,n.data.label,n.data.service,n.data.provider,n.data.config,n.data.description]).sort(),p.edges.map(e=>[e.source,e.target,e.label]).sort(),choice(p,'platform.cloud'),choice(p,'cloud.region'),choice(p,'data.country')]);
45
- return {scope,architecture,estimate:snapshot([architecture,p.estimate||{}])};
45
+ const estimate=snapshot([architecture,p.estimate||{}]);return {scope,architecture,estimate,delivery:snapshot([architecture,estimate,p.revision])};
46
46
  }
47
47
  export function cloudMapping(p){
48
48
  const provider=choice(p,'platform.cloud');
@@ -85,3 +85,13 @@ export function advanceJourney(p,checkpoint){
85
85
  const key=checkpoint==='review'?'architecture':checkpoint;
86
86
  return {...p,extensions:{...p.extensions,journey:{...p.extensions?.journey,[checkpoint]:state.keys[key]}}};
87
87
  }
88
+
89
+ // Explicit completion actions acknowledge a working draft; navigation never does.
90
+ export function acknowledgeStage(p,stage){
91
+ const keys=journeyKeys(p),previous=p.extensions?.journey||{},saved={...previous};
92
+ if(stage==='scope'&&(p.needs?.length||p.brief?.application?.trim()))saved.scope=keys.scope;
93
+ if(stage==='cloud'&&cloudMapping(p).ready)saved.architecture=keys.architecture;
94
+ if(stage==='review')saved.review=keys.architecture;
95
+ if(stage==='delivery')saved.delivery=keys.delivery;
96
+ return {...p,extensions:{...p.extensions,journey:saved}};
97
+ }
package/src/main.jsx CHANGED
@@ -1,9 +1,12 @@
1
+ import {LocationChoices} from './LocationChoices.jsx';
2
+ import {InlineNote} from './HelpNote.jsx';
3
+ import {ProjectChecks} from './ProjectChecks.jsx';
4
+ import {useEngineeringStatus} from './useEngineeringStatus.mjs';
1
5
  import {ChatResizeHandle} from './ChatResizeHandle.jsx';
2
6
  import {copyProject} from './project-copy.mjs';
3
7
  import './journey-workspace.css';
4
8
  import {synchronizeEngineeringIntent,applyArchitectureChange} from './architecture-work.mjs';
5
9
  import {applyArtifactWork} from './artifact-work.mjs';
6
- import {ArchitectureWorkPanel} from './ArchitectureWorkPanel.jsx';
7
10
  import {hasArchitectureDesign} from './design-build-flow.mjs';
8
11
  import {ArchitectureTurn,EarlierArchitectureActivity} from './ArchitectureActivity.jsx';
9
12
  import {recordArchitectureTurn,finishArchitectureTurn,architectureChatEntries} from './architecture-activity.mjs';
@@ -33,7 +36,7 @@ import {OpenAIVoicePanel} from './OpenAIVoicePanel.jsx';
33
36
  import { OutputsView } from "./OutputsView.jsx";
34
37
  import { outputBundle } from "./output-bundle.mjs";
35
38
  import { TerraformView } from "./TerraformView.jsx";
36
- import { journey, advanceJourney, DESIGN_SECTIONS,journeySectionId,completedStages, designSection, journeyAction } from "./journey.mjs";
39
+ import { journey, advanceJourney, acknowledgeStage, DESIGN_SECTIONS,journeySectionId,completedStages, designSection, journeyAction } from "./journey.mjs";
37
40
  import "@xyflow/react/dist/style.css";
38
41
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
39
42
  import { createRoot } from "react-dom/client";
@@ -59,6 +62,8 @@ import {
59
62
  ChevronRight,
60
63
  GitBranch,
61
64
  Maximize2,
65
+ ChevronsLeftRight,
66
+ ChevronsRightLeft,
62
67
  MoreHorizontal,
63
68
  PanelRightClose,
64
69
  PanelRightOpen,
@@ -99,6 +104,8 @@ import {
99
104
  Eraser,
100
105
  LayoutGrid,
101
106
  ArrowLeft,
107
+ ArrowRightToLine,
108
+ ArrowRightFromLine,
102
109
  FolderOpen,
103
110
  Moon,
104
111
  Sun,
@@ -123,12 +130,12 @@ import { withDerived } from "./derive.mjs";
123
130
  import { MemoryPanel, ContextUsed } from "./MemoryPanel.jsx";
124
131
  import { defaultAssessment } from "./engagement.mjs";
125
132
  import { setUsage } from "./estimate.mjs";
126
- import { applyPriceQuotes } from "./pricing.mjs";
133
+ import { applyPriceQuotes, fetchPlanningPrices, storageNodes, lambdaNodes, googleNodes } from "./pricing.mjs";
127
134
  import { applyDecisionUpdates, applyRequirementDrafts, proposeCommentRevisions } from "./consultation.mjs";
128
135
  import { terraformSketch } from "./terraform.mjs";
129
136
  import { WORKFLOWS, conceptSignature, essentialsKnown, nextQuestion, opening, progress, sketch, understand } from "./discovery.mjs";
130
137
  import { review } from "./review.mjs";
131
- import { estimate } from "./estimate.mjs";
138
+ import { setProjectSize, estimate } from "./estimate.mjs";
132
139
  import { DeployView, EstimateView, ProjectsDialog, ReviewView, TestCard } from "./JourneyViews.jsx";
133
140
  import { newRequirement } from "./records.mjs";
134
141
  import { DiscoverView, OptionsView, DecisionsView, MeasuresEditor, RequirementsView } from "./EngagementViews.jsx";
@@ -289,7 +296,7 @@ function BlockNode({ data, selected }) {
289
296
  </span>
290
297
  <span className="blk-text">
291
298
  <strong>{data.label}</strong>
292
- {data.description && <span className="blk-why">{data.description}</span>}
299
+ {data.service&&<small className="blk-service">{data.service}</small>}
293
300
  {(data.origin === "existing" || data.provider !== "tools") && (
294
301
  <small>
295
302
  <i className="pdot" />
@@ -452,7 +459,7 @@ function Linked({ project, id, hideNeeds = false }) {
452
459
  );
453
460
  }
454
461
 
455
- function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewConfirm, onReviewDiscuss, onReviewQuestion, onReviewResolve, onRequirementSave, onWorkModelChange, onChange, onRemove, onOpen, onBack }) {
462
+ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewConfirm, onReviewDiscuss, onReviewQuestion, onReviewResolve, onRequirementSave, onWorkModelChange, onChange, onRemove, onOpen, onBack,onSpec }) {
456
463
  const trace = run?.steps.find((s) => s.id === node.id);
457
464
  const inputs = project.edges.filter((e) => e.target === node.id);
458
465
  const outputs = project.edges.filter((e) => e.source === node.id);
@@ -464,11 +471,11 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
464
471
  <section className="detail" aria-label="Block detail">
465
472
  <div className="detail-navigation">
466
473
  <button className="back" onClick={onBack} aria-label="Back to the board">
467
- <ArrowLeft size={15} /> Back to board <kbd>Esc</kbd>
474
+ <ArrowLeft size={15} /><kbd>Esc</kbd>
468
475
  </button>
469
476
  </div>
470
477
  <div className="detail-col">
471
- <h3>Inputs</h3>
478
+ <h3 aria-label="Inputs" title="Inputs"><ArrowRightToLine size={20}/></h3>
472
479
  {inputs.length ? (
473
480
  inputs.map((e) => (
474
481
  <button key={e.id} className="terminal" onClick={() => onOpen(e.source)}>
@@ -496,9 +503,8 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
496
503
  onChange={(e) => set({ label: e.target.value })}
497
504
  />
498
505
  </div>
499
- <p className="muted">{node.data.description || "No description."}</p>
506
+ <button className="link" onClick={onSpec}>Specification ↗</button>
500
507
  <BlockRequirements project={project} nodeId={node.id} busy={busy} onSave={onRequirementSave}/>
501
- {project.extensions?.architectureWork?.artifactMappings?.length>0&&<ArchitectureWorkPanel project={project} onChange={onWorkModelChange} busy={busy}/>}
502
508
  <ComponentReview key={node.id} item={reviewItems.find(r=>r.id===node.id)} items={reviewItems} busy={busy} onOpen={onOpen} onConfirm={onReviewConfirm} onDiscuss={onReviewDiscuss} onQuestion={onReviewQuestion} onResolve={onReviewResolve} onDetailAnswer={patch=>onChange({...node,data:{...node.data,...patch,...(patch.config?{config:{...node.data.config,...patch.config}}:{})}})}/>
503
509
  <details className="component-settings"><summary>Component settings</summary><div className="fields">
504
510
  <label>
@@ -590,7 +596,7 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
590
596
  </button>
591
597
  </div>
592
598
  <div className={`detail-col ${probe ? "probed" : ""}`}>
593
- <h3>Outputs</h3>
599
+ <h3 aria-label="Outputs" title="Outputs"><ArrowRightFromLine size={20}/></h3>
594
600
  {outputs.length ? (
595
601
  outputs.map((e) => (
596
602
  <button key={e.id} className="terminal" onClick={() => onOpen(e.target)}>
@@ -640,7 +646,7 @@ const welcome = (project) => ({
640
646
  role: "assistant",
641
647
  text: project.nodes.length || project.brief?.application?.trim()
642
648
  ? `Good to see you again — let's keep going.\n\n${opening(project)}`
643
- : `Hi! What would you like to build? For example, an assistant that answers questions using your company’s documents, or a chatbot for your team. “Not sure” is always a fine answer along the way.`,
649
+ : `What would you like to build?`,
644
650
  });
645
651
 
646
652
  const THEME = "atmem-architect-theme";
@@ -654,6 +660,8 @@ function savedTheme() {
654
660
  }
655
661
  document.documentElement.dataset.theme = savedTheme();
656
662
 
663
+ 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:[]};}}
664
+ 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])]);
657
665
  function App() {
658
666
  const [project, setProject] = useState(readSaved);
659
667
  const projectRef = useRef(project);
@@ -676,9 +684,9 @@ function App() {
676
684
  [level, setLevel] = useState(initialCanvasView.level),
677
685
  [focus, setFocus] = useState(initialCanvasView.focus),
678
686
  [selected, setSelected] = useState(initialCanvasView.selected),
679
- [messages, setMessages] = useState(() => conversationMessages(projectRef.current, [welcome(projectRef.current)])),
687
+ [messages, setMessagesState] = useState(() => conversationMessages(projectRef.current, [welcome(projectRef.current)])),
680
688
  [reviewView, setReviewView] = useState("findings"),
681
- [input, setInput] = useState(""),
689
+ [input, setInput] = useState(()=>{try{return localStorage.getItem('atmem-chat-draft:'+projectRef.current.id)||'';}catch{return '';}}),
682
690
  [pending, setPending] = useState(null),
683
691
  [busy, setBusy] = useState(false),
684
692
  [scenario, setScenario] = useState("happy"),
@@ -699,6 +707,19 @@ function App() {
699
707
  [probe, setProbe] = useState(null),
700
708
  [snapshot, setSnapshot] = useState(null),
701
709
  [measured, setMeasured] = useState({});
710
+ const setMessages=useCallback(updater=>setMessagesState(previous=>{
711
+ const next=typeof updater==='function'?updater(previous):updater;
712
+ if(typeof updater!=='function')return next;
713
+ return next.map((m,i)=>m.createdAt||previous.includes(m)||(i<previous.length&&previous[i]?.role===m.role&&previous[i]?.text===m.text)?m:{...m,createdAt:new Date().toISOString()});
714
+ }),[]);
715
+ const draftOwner=useRef(project.id);
716
+ useEffect(()=>{
717
+ if(draftOwner.current!==project.id){draftOwner.current=project.id;try{setInput(localStorage.getItem('atmem-chat-draft:'+project.id)||'');}catch{setInput('');}return;}
718
+ try{localStorage.setItem('atmem-chat-draft:'+project.id,input);}catch{}
719
+ },[project.id,input]);
720
+ const [chatView,setChatView]=useState(()=>readChatView(project.id));
721
+ useEffect(()=>setChatView(readChatView(project.id)),[project.id]);
722
+ function changeChatView(value){setChatView(value);try{localStorage.setItem('atmem-chat-view:'+project.id,JSON.stringify(value));}catch{}}
702
723
  const messagesRef=useRef(messages);messagesRef.current=messages;
703
724
  const [session, setSession] = useState(null),
704
725
  [connection, setConnection] = useState(readConnection),
@@ -718,6 +739,36 @@ function App() {
718
739
  useEffect(()=>{saveCanvasView(project,{level,focus,selected,positions:topologyPositions});},[project.id,level,focus,selected,topologyPositions]);
719
740
  const emulation=useEmulation(project,environment,session);
720
741
  const emulationWorking=emulation.running||(agentBusy&&!!emulation.result);
742
+ const [pricingState,setPricingState]=useState({busy:false,errors:[]});
743
+ const [generationPending,setGenerationPending]=useState(false);
744
+ const [checking,setChecking]=useState(false),[checkReceipt,setCheckReceipt]=useState(null);
745
+ const engineeringStatus=useEngineeringStatus(session,project.id);
746
+ const [architectAction,setArchitectAction]=useState('Architect is working…');
747
+ const pricingRequest=useRef(0);
748
+ const pricingAttempt=useRef(new Set());
749
+ const pricingKey=pricingSignature(project);
750
+ async function refreshEstimate(selected){
751
+ if(!session?.token)return;
752
+ const requested=selected.estimate?.projectSize?selected:setProjectSize(selected,'small');
753
+ if(requested!==selected)updateEngagement(requested);
754
+ const request=++pricingRequest.current;pricingAttempt.current.add(pricingSignature(requested));
755
+ const baseline=JSON.stringify([requested.estimate?.prices,requested.estimate?.details]);
756
+ const identity=JSON.stringify([requested.id,requested.estimate?.projectSize,requested.estimate?.usage,requested.facts,requested.nodes]);
757
+ setPricingState({busy:true,errors:[]});
758
+ try{
759
+ const result=await fetchPlanningPrices(session,requested),live=projectRef.current;
760
+ if(request!==pricingRequest.current)return;
761
+ if(JSON.stringify([live.id,live.estimate?.projectSize,live.estimate?.usage,live.facts,live.nodes])!==identity||JSON.stringify([live.estimate?.prices,live.estimate?.details])!==baseline){setPricingState({busy:false,errors:[]});return;}
762
+ // Merge only the fetched pricing fields; preserve concurrent design and conversation edits.
763
+ updateEngagement({...live,estimate:{...live.estimate,prices:result.project.estimate?.prices,details:result.project.estimate?.details}});
764
+ setPricingState({busy:false,errors:result.errors});
765
+ }catch(error){if(request===pricingRequest.current)setPricingState({busy:false,errors:[{reason:error.message}]});}
766
+ }
767
+ useEffect(()=>{
768
+ if(!session?.token||!project.nodes.length)return;
769
+ if(pricingAttempt.current.has(pricingKey))return;
770
+ pricingAttempt.current.add(pricingKey);void refreshEstimate(project);
771
+ },[mode,session?.token,pricingKey]);
721
772
  const [chatWidth,setChatWidth]=useState(()=>{try{const n=Number(localStorage.getItem('atmem-architect-chat-width'));return Number.isFinite(n)&&n>=320&&n<=900?n:360;}catch{return 360;}});
722
773
  function resizeChat(width){setChatWidth(width);try{localStorage.setItem('atmem-architect-chat-width',String(width));}catch{}}
723
774
  const [dismissedStarts,setDismissedStarts]=useState(()=>{try{const ids=JSON.parse(localStorage.getItem('atforma-dismissed-starts-v1')||'[]');return Array.isArray(ids)?ids.filter(id=>typeof id==='string').slice(-64):[];}catch{return [];}});
@@ -1239,7 +1290,7 @@ function App() {
1239
1290
  setInput("");
1240
1291
  }
1241
1292
  if(origin==='chat'){setPending(null);if(!intakeOptions.alreadyLogged)setAttachments([]);}
1242
- if (origin === "chat") setChatOpen(true);
1293
+ setChatOpen(true);
1243
1294
  if (!connection) {
1244
1295
  setRequestState(requestIds,'failed');
1245
1296
  setModal("welcome");
@@ -1261,6 +1312,7 @@ function App() {
1261
1312
  const controller = new AbortController();
1262
1313
  agentAbort.current = controller;
1263
1314
  chatScroll.followLatest();
1315
+ setArchitectAction(origin==='block-review'?`Reviewing ${reviewTarget?.label||'the block'}…`:origin==='chat'?'Architect is working…':'Updating the design…');
1264
1316
  setAgentBusy(true);
1265
1317
  const activityId=session.architectureActivityEnabled?crypto.randomUUID():null;
1266
1318
  try {
@@ -1528,7 +1580,8 @@ function App() {
1528
1580
  };
1529
1581
  const downloadBundle = bundle => {
1530
1582
  download(zipSync(Object.fromEntries(Object.entries(bundle.files).map(([name,content])=>[name,typeof content === "string"?strToU8(content):content]))),bundle.filename,"application/zip");
1531
- notify("Reviewed output package downloaded.");
1583
+ if(bundle.project?.id===projectRef.current.id)updateEngagement(acknowledgeStage(projectRef.current,'delivery'));
1584
+ notify("ZIP downloaded.");
1532
1585
  };
1533
1586
  const exportPack = async () => {
1534
1587
  try {
@@ -1764,14 +1817,28 @@ function App() {
1764
1817
  const onBoard = testing || (mode === "design" && designView === "board");
1765
1818
  const diff = pending?.next ? proposalDiff(project, pending.next) : null;
1766
1819
 
1820
+ const specificationChecks=(
1821
+ <ReviewView expanded
1822
+ project={project}
1823
+ findings={findings}
1824
+ onChange={updateEngagement}
1825
+ onOpen={(id) => {
1826
+ setMode("design");
1827
+ setDesignView("board");
1828
+ openBlock(id);
1829
+ }}
1830
+ onAddRequirement={(category) => {
1831
+ setMode("design");setDesignView("requirements");setChatOpen(true);
1832
+ send(`Help us establish the ${category} requirement for this architecture. Explain why it matters and ask for the target we actually need; do not insert a default SLA.`,"workflow",{mode:"design",view:"requirements"});
1833
+ }}
1834
+ />
1835
+ );
1767
1836
  return (
1768
1837
  <div style={{"--chat-panel-width":`${chatWidth}px`}} className={`app ${chatOpen&&!buildView ? "" : "chat-closed"} ${buildView?"build-workspace":""}`}>
1769
1838
  <header className="bar">
1770
- <button className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
1839
+ <button aria-label="About AtMem AI Architect" className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
1771
1840
  <Mark />
1772
- <span>
1773
- AtMem.ai<span className="brand-divider">|</span>AI Architect
1774
- </span>
1841
+
1775
1842
  </button>
1776
1843
  <nav className="crumbs" aria-label="Level"><span className="workspace-label">PROJECT</span>
1777
1844
  <button className="projects-btn" aria-label="Projects" title="Your projects" onClick={() => (refreshProjects(), setModal("projects"))}>
@@ -1815,10 +1882,10 @@ function App() {
1815
1882
  <button role="menuitem" onClick={() => setModal("templates")} disabled={busy}><LayoutTemplate size={16}/>Templates</button>
1816
1883
  <hr/><button role="menuitem" onClick={() => setModal("import")} disabled={busy}><GitBranch size={16}/>Import diagram</button>
1817
1884
  <button role="menuitem" onClick={() => setModal("project-file")} disabled={busy}><FolderOpen size={16}/>Open project file</button>
1818
- <hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><BrainCircuit size={16}/>AtMem dashboard</button>
1885
+ <hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><BrainActivity size={18} active={false}/>AtMem memory</button>
1819
1886
  <button role="menuitem" onClick={() => setModal("start-over")} disabled={busy || agentBusy}><RotateCcw size={16}/>Start over</button>
1820
1887
  <hr />
1821
- <button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>Connections</button>
1888
+ <button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>AI providers</button>
1822
1889
  <button role="menuitem" onClick={() => setModal("licenses")}><ShieldCheck size={16}/>Licences</button>
1823
1890
  </div>
1824
1891
  )}
@@ -1826,7 +1893,7 @@ function App() {
1826
1893
 
1827
1894
  <section className="journey-wizard unified-journey" aria-label="Architecture journey">
1828
1895
  <nav className="stage-tabs" role="tablist" aria-label="Project stages">{DESIGN_SECTIONS.map((section,i)=><button key={section.id} role="tab" aria-label={section.label} aria-selected={activeSection.id===section.id} className={completedStages(project)[section.id]?'stage-complete':''} onClick={()=>visitJourney(section.id)} title={completedStages(project)[section.id]?'Working draft acknowledged':'Not yet complete'}><span>{completedStages(project)[section.id]?'✓':i+1}</span>{section.label}</button>)}</nav>
1829
- {nextAction&&<div className="journey-current"><p>{guidance.reason}</p><button className="btn primary" disabled={busy||agentBusy||!!pending} onClick={()=>nextAction.kind==='navigate'?visitJourney(nextAction.section):nextAction.kind==='continue'?continueJourney():followJourney()}>{nextAction.label}</button></div>}
1896
+ {nextAction&&<div className="journey-current"><button className="btn primary" disabled={busy||agentBusy||!!pending} onClick={()=>nextAction.kind==='navigate'?visitJourney(nextAction.section):nextAction.kind==='continue'?continueJourney():followJourney()}>{nextAction.label}</button></div>}
1830
1897
  </section>
1831
1898
  <main
1832
1899
  id="canvas"
@@ -1846,7 +1913,7 @@ function App() {
1846
1913
  {level!=="block"&&<aside className="board-components component-navigator" data-expanded={treeOpen} aria-label="Board components"><button className="btn" aria-label="Toggle components and layers" aria-expanded={treeOpen} onClick={()=>setTreeOpen(!treeOpen)}><LayoutGrid size={16}/> Components &amp; layers</button>
1847
1914
  {treeOpen&&<nav className="board-component-tree" aria-label="Component tree"><strong>{project.name}</strong><div className="tree-views"><button disabled={!blockReviews.length} onClick={()=>{reviewBlock((blockReviews.find(r=>r.status!=='reviewed')||blockReviews[0]).id);}}><CheckCheck size={16}/> Review blocks · {blockReviews.filter(r=>r.status==='reviewed').length}/{blockReviews.length}</button></div>{[...new Map(project.nodes.filter(n=>n.data.kind!=='note').map(n=>[layerOf(n).id,layerOf(n)])).values()].map(layer=><details open key={layer.id}><summary>{layer.label}</summary>{project.nodes.filter(n=>n.data.kind!=='note'&&layerOf(n).id===layer.id).map(n=><button key={n.id} aria-current={selected===n.id?'true':undefined} onClick={()=>{setMode('design');setDesignView('board');setFocus(null);setLevel('flow');setSelected(n.id);setTimeout(()=>fitView({nodes:[{id:n.id}],padding:1,maxZoom:1.2,duration:350}),100);}}><Database size={14}/><span>{n.data.label}<small>{n.data.service||n.data.provider}</small><small className={`tree-review-state ${blockReviews.find(r=>r.id===n.id)?.status}`}>{blockReviews.find(r=>r.id===n.id)?.statusLabel}</small></span></button>)}</details>)}</nav>}
1848
1915
  {treeOpen&&<button className="btn" disabled={busy} onClick={()=>setPalette(true)}><Plus size={16}/> Add component</button>}</aside>}
1849
- {!hasDesign&&!palette&&!dismissedStarts.includes(project.id)&&<section className="board-start" aria-label="Start your project"><button type="button" className="icon board-start-close" aria-label="Dismiss welcome card" title="Dismiss welcome card" onClick={dismissStart}><X size={16}/></button><h2>What would you like to create?</h2><p>Describe your idea in chat, upload existing requirements or a diagram, or open a saved project.</p><div className="row"><button className="btn primary" onClick={()=>{dismissStart();setChatOpen(true);document.querySelector('.composer textarea, .chat textarea')?.focus();}}>Start chatting</button><button className="btn" onClick={()=>{setChatOpen(true);requirementsInput.current.click();}}>Upload requirements or diagram</button><button className="btn" onClick={()=>setModal("project-file")}>Open saved project file</button></div><small>Word (.docx), PDF, text, Markdown, draw.io, images and Architect project JSON. Architect reads your material and asks questions in chat.</small></section>}
1916
+ {!hasDesign&&!palette&&!dismissedStarts.includes(project.id)&&<section className="board-start" aria-label="Start your project"><button type="button" className="icon board-start-close" aria-label="Dismiss welcome card" title="Dismiss welcome card" onClick={dismissStart}><X size={16}/></button><h2>What would you like to create?</h2><div className="row"><button className="btn primary" onClick={()=>{dismissStart();setChatOpen(true);document.querySelector('.composer textarea, .chat textarea')?.focus();}}>Start chatting</button><button className="btn" onClick={()=>{setChatOpen(true);requirementsInput.current.click();}}>Upload requirements or diagram</button><button className="btn" onClick={()=>setModal("project-file")}>Open project</button></div><InlineNote label="Supported starting files">Word, PDF, text, Markdown, draw.io, images and Architect project JSON.</InlineNote></section>}
1850
1917
  <ReactFlow
1851
1918
  nodes={canvasNodes}
1852
1919
  edges={canvasEdges}
@@ -2017,6 +2084,7 @@ function App() {
2017
2084
  onWorkModelChange={changeArchitectureWork}
2018
2085
  onRemove={() => removeNode(selectedNode.id)}
2019
2086
  onOpen={(id) => openBlock(id)}
2087
+ onSpec={()=>{visitJourney('scope');setTimeout(()=>document.getElementById('component-spec-'+selectedNode.id)?.scrollIntoView({block:'start'}),0);}}
2020
2088
  onBack={goUp}
2021
2089
  />
2022
2090
  )}
@@ -2093,31 +2161,15 @@ function App() {
2093
2161
 
2094
2162
  </div>
2095
2163
  <div className="review-workspace" hidden={mode!=="review"&&mode!=="estimate"}>
2096
- <header className="review-intro"><h2>Review and verify</h2><p>Review the specification, tradeoffs and costs, then generate the project and inspect its checks. Delivery packages the resulting files and evidence.</p></header>
2097
- <details className="review-group"><summary>Specification, risks and tradeoffs</summary>
2098
- {mode === "review" && reviewView === "findings" && (
2099
- <ReviewView
2100
- project={project}
2101
- findings={findings}
2102
- onChange={updateEngagement}
2103
- onOpen={(id) => {
2104
- setMode("design");
2105
- setDesignView("board");
2106
- openBlock(id);
2107
- }}
2108
- onAddRequirement={(category) => {
2109
- setMode("design");setDesignView("requirements");setChatOpen(true);
2110
- send(`Help us establish the ${category} requirement for this architecture. Explain why it matters and ask for the target we actually need; do not insert a default SLA.`,"workflow",{mode:"design",view:"requirements"});
2111
- }}
2112
- />
2113
- )}
2114
- </details>
2115
- <details className="review-group"><summary>Costs and usage assumptions</summary>
2116
- {["review","estimate"].includes(mode) && <EstimateView project={project} result={costs} onChange={updateEngagement} busy={busy||agentBusy||!!pending} onEstimate={(sized)=>{setChatOpen(true);send(`Prepare a rough monthly estimate for the saved design using the ${sized.estimate?.projectSize||'selected'} project-size planning assumptions. Preserve explicit business usage, requirements, provider and region. Use available sourced price tools automatically; never ask the client to research unit prices. Explain the subtotal and excluded charges, and ask at most one material unresolved question. Do not invent rates or treat logical security controls as separately billable services without evidence. Planning usage: ${JSON.stringify(estimate(sized).usage)}. Keep the design unchanged.`,"workflow",null,null,{estimateOnly:true});}} />}
2117
- </details>
2118
- {mode==='review'&&session?.engineeringEnabled&&<section className="review-generation" aria-label="Generation and verification"><EngineeringPanel key={project.id} project={project} 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>}
2164
+ <header className="review-intro"><h2>Review</h2></header>
2165
+ <div className="review-design-status"><ShieldCheck size={18}/><span>{findings.filter(f=>f.status==='open').length} design items</span><button className="icon" aria-label="Open design checks in specification" title="Specification" onClick={()=>visitJourney('scope')}><Code2 size={16}/></button></div>
2166
+ <section className="review-costs" aria-label="Project costs">
2167
+ {["review","estimate"].includes(mode) && <EstimateView project={project} result={costs} onChange={updateEngagement} busy={busy||agentBusy||!!pending} onEstimate={refreshEstimate} 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'});}}} />}
2168
+ </section>
2169
+ {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>}
2119
2170
  {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>}
2120
- {mode==='review'&&<details className="review-infrastructure"><summary>Generated infrastructure and configuration checks</summary><TerraformView project={project} environment={environment} session={session} discussionBusy={busy||agentBusy||!!pending} onDelivery={()=>visitJourney('delivery')} onBack={()=>visitJourney('board')}/></details>}
2171
+ {mode==='review'&&<ProjectChecks receipt={checkReceipt} project={project} environment={environment} session={session} onReceipt={setCheckReceipt} onBusy={setChecking}/>}
2172
+ {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.complete?{...next,extensions:{...next.extensions,journey:{...next.extensions.journey,estimate:journey(next).keys.estimate}}}:next);visitJourney('delivery');}}>Complete →</button></footer>}
2121
2173
  </div>
2122
2174
 
2123
2175
 
@@ -2125,16 +2177,16 @@ function App() {
2125
2177
 
2126
2178
  {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>}
2127
2179
 
2128
- {mode === "design" && designView === "outputs" && <OutputsView 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")} />}
2180
+ {mode === "design" && designView === "outputs" && <OutputsView onNavigate={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);requestAnimationFrame(()=>requestAnimationFrame(()=>{const element=document.querySelector(selector);if(element){element.setAttribute('tabindex','-1');element.focus({preventScroll:true});element.scrollIntoView({block:'start'});}}));}} checkReceipt={checkReceipt} onDownloaded={()=>updateEngagement(acknowledgeStage(projectRef.current,'delivery'))} 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")} />}
2129
2181
  {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")} />}
2130
2182
  <div className="choices-workspace" hidden={!(mode==="design"&&["options","decisions","location"].includes(designView))}>
2131
2183
  {mode === "design" && ["options","decisions"].includes(designView) && (
2132
- <OptionsView project={project} busy={busy||agentBusy||!!pending} onChoose={chooseCloud} onContinue={()=>visitJourney("review")} onPreview={(provider)=>{setDesignView("board");setLevel("flow");setChatOpen(true);send(`Preview a concrete ${OPTION_NAMES[provider]} implementation of our working scope, using actual named services in data.service and explicit connections. Keep existing systems and comments. Use the known country/residency constraints, or leave region undecided and ask the country question. This is an alternative preview, not a cloud choice or deployment. Return a graph proposal and explain key integration assumptions.`,"preview",{mode:"design",view:"board"});}} onDiscuss={(question) => { setChatOpen(true); send(question, "workflow"); }} />
2184
+ <OptionsView project={project} busy={busy||agentBusy||!!pending} onChoose={chooseCloud} onContinue={()=>{updateEngagement(acknowledgeStage(projectRef.current,'cloud'));visitJourney('review');}} onPreview={(provider)=>{setDesignView("board");setLevel("flow");setChatOpen(true);send(`Preview a concrete ${OPTION_NAMES[provider]} implementation of our working scope, using actual named services in data.service and explicit connections. Keep existing systems and comments. Use the known country/residency constraints, or leave region undecided and ask the country question. This is an alternative preview, not a cloud choice or deployment. Return a graph proposal and explain key integration assumptions.`,"preview",{mode:"design",view:"board"});}} onDiscuss={(question) => { setChatOpen(true); send(question, "workflow"); }} />
2133
2185
  )}
2134
- {mode === "design" && ["options","decisions","location"].includes(designView) && <section className="doc" aria-label="Country and region"><header className="doc-head"><h2>Country &amp; region</h2><p className="muted">Where users and data are located affects residency, regional availability and cost. Proposed locations require review before they become decisions.</p></header>{['platform.cloud','data.country','cloud.region'].map(subject=>{const decision=[...(project.decisions||[])].reverse().find(d=>d.subject===subject&&d.status==='Accepted');return <p key={subject}><strong>{{'platform.cloud':'Cloud','data.country':'Country / data residency','cloud.region':'Region'}[subject]}:</strong> {decision?.decision||'Not yet agreed'}</p>;})}<button className="btn primary" disabled={busy||agentBusy||!!pending} onClick={()=>{setChatOpen(true);send('Review the accepted cloud, country and region choices for this project. Explain what is known, then ask one important unresolved location or data residency question. Keep the diagram unchanged until we agree a change. Do not silently choose a country or region.','workflow');}}>Discuss country and region</button><button className="link" onClick={()=>visitJourney('review')}>Continue to Review</button></section>}
2186
+ {mode === "design" && ["options","decisions","location"].includes(designView) && <LocationChoices project={project} onChange={commit} onDiscuss={()=>{setChatOpen(true);send('Review the accepted cloud, data country and cloud region. Ask one material unresolved location question. Preserve selected regions and residency.','workflow');}}/>}
2135
2187
  </div>
2136
2188
  {mode === "design" && designView === "requirements" && (
2137
- <RequirementsView onUpload={()=>{setChatOpen(true);requirementsInput.current.click();}} onOpenProject={()=>setModal("project-file")} project={project} onChange={updateEngagement} reviewing={agentBusy} onReview={(comments)=>{setChatOpen(true);send("Propose revisions for these user comments on the requirements document. The document will remain unchanged until the user applies each suggestion. Explain the proposed change; never say you have already changed the document. Return requirementUpdates for the existing IDs where the feedback is clear, quoting the user's comment exactly in sourceText. Preserve other requirements and the diagram. Ask one question if a comment is ambiguous.\n" + comments.map(c=>`Requirement ${c.requirementId}${c.selectedText ? ` (selected text: ${c.selectedText.slice(0,300)})` : ""} — comment: ${c.text}`).join("\n"), "document");}} onDiscuss={(question) => { setChatOpen(true); send(question, "workflow"); }} />
2189
+ <RequirementsView checks={specificationChecks} onContinue={()=>{updateEngagement(acknowledgeStage(projectRef.current,'scope'));visitJourney('cloud');}} onUpload={()=>{setChatOpen(true);requirementsInput.current.click();}} onOpenProject={()=>setModal("project-file")} project={project} onChange={updateEngagement} reviewing={agentBusy} onReview={(comments)=>{setChatOpen(true);send("Propose revisions for these user comments on the requirements document. The document will remain unchanged until the user applies each suggestion. Explain the proposed change; never say you have already changed the document. Return requirementUpdates for the existing IDs where the feedback is clear, quoting the user's comment exactly in sourceText. Preserve other requirements and the diagram. Ask one question if a comment is ambiguous.\n" + comments.map(c=>`Requirement ${c.requirementId}${c.selectedText ? ` (selected text: ${c.selectedText.slice(0,300)})` : ""} — comment: ${c.text}`).join("\n"), "document");}} onDiscuss={(question) => { setChatOpen(true); send(question, "workflow"); }} />
2138
2190
  )}
2139
2191
  </main>
2140
2192
 
@@ -2142,9 +2194,9 @@ function App() {
2142
2194
  {chatOpen&&!buildView && (
2143
2195
  <aside className="chat" aria-label="Chat"><ChatResizeHandle width={chatWidth} onChange={resizeChat}/>
2144
2196
  <div className="chat-head">
2145
- <div className="chat-heading-row"><strong className="chat-title">
2146
- <Mark size={18} /> AI Architect
2147
- </strong><button className="link small chat-width-toggle" onClick={()=>resizeChat(chatWidth>360?360:Math.max(320,Math.min(720,window.innerWidth-360)))} title="You can also drag the left edge of the chat panel">{chatWidth>360?"Narrow":"Widen"}</button><button className="icon" title="Hide chat" aria-label="Hide chat" onClick={()=>setChatOpen(false)}><PanelRightClose size={16}/></button></div>
2197
+ <div className="chat-heading-row"><strong className="chat-title" aria-label="AI Architect">
2198
+ <Mark size={24} />
2199
+ </strong><button className="icon" title="Clear chat view" aria-label="Clear chat view" disabled={busy||agentBusy||intake.length>0||messages.length<=chatView.start} onClick={()=>changeChatView({start:messages.length,turnIds:(project.extensions?.architectureActivity?.turns||[]).map(turn=>turn.id)})}><Trash2 size={16}/></button>{(chatView.start>0||chatView.turnIds.length>0)&&<button className="icon" title="Restore chat history" aria-label="Restore chat history" onClick={()=>changeChatView({start:0,turnIds:[]})}><Undo2 size={16}/></button>}<button className="icon chat-width-toggle" onClick={()=>resizeChat(chatWidth>360?360:Math.max(320,Math.min(720,window.innerWidth-360)))} aria-label={chatWidth>360?"Narrow chat":"Widen chat"} title={chatWidth>360?"Narrow chat":"Widen chat"}>{chatWidth>360?<ChevronsRightLeft size={17}/>:<ChevronsLeftRight size={17}/>}</button><button className="icon" title="Hide chat" aria-label="Hide chat" onClick={()=>setChatOpen(false)}><PanelRightClose size={16}/></button></div>
2148
2200
  <label className="live-toggle" title="On: changes are drawn as we talk, and you can undo any of them. Off: they wait as previews for you to apply.">
2149
2201
  <input
2150
2202
  type="checkbox"
@@ -2158,13 +2210,13 @@ function App() {
2158
2210
  />
2159
2211
  Draw as we talk
2160
2212
  </label>
2161
- <button className="model" onClick={() => setModal("connections")} title="Connections">
2213
+ <button className="model" onClick={() => setModal("connections")} title="AI providers">
2162
2214
  <span className={`led ${connection && session ? "on" : ""}`} />
2163
2215
  {connection ? `${connection.model}${connection.effort ? ` · ${EFFORT_LABEL[connection.effort] || connection.effort}` : ""}` : "Connect a model"}
2164
2216
  </button>
2165
2217
  </div>
2166
2218
  <div className="skills" aria-label="Active skills">
2167
- <button className="known-bar" disabled={!session} aria-label="AtMem memory" title="Open AtMem memory, confirmed decisions and sync activity" onClick={()=>setModal("memory")}><BrainCircuit size={15}/>{memoryLabel}<ChevronRight size={14}/></button>
2219
+ <button className="known-bar" disabled={!session} aria-label="AtMem memory" title="Open AtMem memory, confirmed decisions and sync activity" onClick={()=>setModal("memory")}><BrainActivity size={18} active={false}/>{memoryLabel}<ChevronRight size={14}/></button>
2168
2220
  <button
2169
2221
  className={`known-bar ${peek === "known" ? "on" : ""}`}
2170
2222
  aria-label="Design context" aria-expanded={peek === "known"} title="Review what is known, assumed or still open"
@@ -2207,14 +2259,14 @@ function App() {
2207
2259
  )}
2208
2260
  <div className="messages" ref={chatScroll.viewport}>
2209
2261
  <div className="message-list" ref={chatScroll.content}>
2210
- {architectureChatEntries(project,messages).map(entry=>{
2262
+ {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=>{
2211
2263
  if(entry.kind==='activity-error')return <p key="activity-error" role="alert">{entry.error}</p>;
2212
2264
  if(entry.kind==='legacy-activity')return <EarlierArchitectureActivity key={entry.id} entries={entry.entries} projectId={project.id} session={session}/>;
2213
2265
  if(entry.kind==='activity')return <ArchitectureTurn key={project.id+':'+entry.turn.id} turn={entry.turn} number={entry.number} projectId={project.id} session={session} active={agentBusy&&entry.turn.clientStatus==='submitted'&&entry.turn.id===latestArchitectureTurn?.id} working={agentBusy&&entry.turn.clientStatus==='submitted'} onStop={()=>agentAbort.current?.abort()}/>;
2214
- const m=entry.message,i=entry.index;
2266
+ const m=entry.message,i=entry.index,turn=project.extensions?.architectureActivity?.turns?.find(t=>t.id===m.activityId),messageTime=m.createdAt||(m.role==='assistant'?turn?.completedAt:null)||turn?.startedAt;
2215
2267
  return (
2216
2268
  <div key={i} className={`msg ${m.role}`}>
2217
- <ChatMessage text={m.text}/>
2269
+ <header className="message-meta"><span>{m.role==='user'?'You':'Architect'}</span><time dateTime={messageTime||undefined} title={messageTime?new Date(messageTime).toLocaleString():'No timestamp recorded'}>{messageTime?new Date(messageTime).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}):'—'}</time></header><ChatMessage text={m.text} concise={m.role==='assistant'}/>
2218
2270
  {(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>}
2219
2271
  {!!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>}
2220
2272
  {m.fix && (
@@ -2260,15 +2312,9 @@ function App() {
2260
2312
  )}
2261
2313
  </div>
2262
2314
  </div>
2315
+ {chatScroll.away&&<button className="chat-jump-latest" aria-label="Jump to latest message" onClick={chatScroll.followLatest}>↓</button>}
2263
2316
  {!!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>}
2264
2317
  <div className="prompts">
2265
- {mode === "design" && brief(project) && ask && (
2266
- <>
2267
- <button disabled={busy || agentBusy} onClick={() => send("Not sure")}>Not sure</button>
2268
- <button onClick={() => setMessages((m) => [...m, { role: "assistant", text: `Why I ask: ${ask.why || "it shapes the design."}` }])}>Why?</button>
2269
- <button onClick={skipQuestion}>Skip</button>
2270
- </>
2271
- )}
2272
2318
  {mode === "design" && project.workflow && !project.nodes.length && !pending && (
2273
2319
  <button className="primary-chip" disabled={busy || agentBusy} onClick={sketchIt}>Sketch it →</button>
2274
2320
  )}
@@ -2278,6 +2324,7 @@ function App() {
2278
2324
  </button>
2279
2325
  ))}
2280
2326
  </div>
2327
+ {(agentBusy||generationPending||['starting','running'].includes(engineeringStatus?.state)||pricingState.busy||checking)&&<div className="chat-working-status" role="status" aria-live="polite"><BrainActivity size={26}/><span>{agentBusy?architectAction:generationPending?'Starting project generation…':['starting','running'].includes(engineeringStatus?.state)?'Generating project files…':checking?'Running checks…':'Calculating costs…'}</span></div>}
2281
2328
  {readingAttachments&&<p className="attachment-status" role="status"><BrainActivity/> Reading files…</p>}
2282
2329
  {attachmentError&&<p className="attachment-status" role="alert">{attachmentError}</p>}
2283
2330
  {!!attachments.length&&<div className="attachment-tray" aria-label="Attachments ready to send">{attachments.map(a=><div className="attachment-chip" key={a.id}>{a.data&&a.mime.startsWith('image/')?<img src={`data:${a.mime};base64,${a.data}`} alt=""/>:<Paperclip size={16}/>}<span>{a.name}<small>{!a.data?'Original missing · attach again':a.mime.startsWith('image/')?'Image':a.mime==='application/pdf'||a.mime.includes('wordprocessingml')?'Text will be extracted; embedded images omitted':'Document text'}</small></span><button className="icon" aria-label={`Remove ${a.name}`} onClick={()=>setAttachments(list=>list.filter(item=>item.id!==a.id))}><X size={14}/></button></div>)}<p>Sent to your selected architect with this message. Original files are not stored in project files.</p></div>}
@@ -2342,11 +2389,11 @@ function App() {
2342
2389
  )}
2343
2390
  {modal && (
2344
2391
  <div className="backdrop" onMouseDown={(e) => e.target === e.currentTarget && !(modal === "welcome" && !connection) && setModal(null)}>
2345
- <section className="modal" role="dialog" aria-modal="true" aria-label={modal}>
2392
+ <section className={`modal ${modal==="memory"?"memory-modal":""}`} role="dialog" aria-modal="true" aria-label={modal}>
2346
2393
  {!(modal === "welcome" && !connection) && <button className="icon modal-close" onClick={() => setModal(null)} aria-label="Close dialog">
2347
2394
  <X size={16} />
2348
2395
  </button>}
2349
- {modal === "project-file" ? (<><h2>Use a saved project</h2><p>Open the original project, or create a separate project from its requirements and design.</p><button className="btn" onClick={()=>{fileImportAsCopy.current=false;fileRef.current.click();}}>Open saved project</button><button className="btn primary" onClick={()=>{fileImportAsCopy.current=true;fileRef.current.click();}}>Start a new project from a saved file</button></>) : modal === "memory" ? (
2396
+ {modal === "project-file" ? (<><h2>Open project</h2><button className="btn primary" onClick={()=>{fileImportAsCopy.current=false;fileRef.current.click();}}>Open project</button></>) : modal === "memory" ? (
2350
2397
  <MemoryPanel key={project.id} session={session} project={project} onError={notify} onStatus={status=>setMemorySync({projectId:project.id,...status})} />
2351
2398
  ) : modal === "templates" ? (
2352
2399
  <TemplateDialog onLoad={loadDesign} />
@@ -2362,7 +2409,7 @@ function App() {
2362
2409
  environment={environment}
2363
2410
  />
2364
2411
  ) : modal === "projects" ? (
2365
- <div><div className="row"><button className="btn" onClick={()=>setModal("project-file")}>Open or copy a project file</button><button className="btn" onClick={saveProjectFile}>Save project backup (JSON)</button></div><ProjectsDialog
2412
+ <div><div className="row"><button className="btn" onClick={()=>setModal("project-file")}>Open project</button><button className="btn" onClick={saveProjectFile}>Save project</button></div><ProjectsDialog
2366
2413
  list={saved}
2367
2414
  folder={saved.folder}
2368
2415
  currentId={project.id}
@@ -2412,36 +2459,12 @@ function App() {
2412
2459
  />
2413
2460
  ) : modal === "licenses" ? (
2414
2461
  <>
2415
- <h2>Licences</h2>
2416
- <dl className="licences">
2417
- <dt>OpenArchFlow</dt>
2418
- <dd>
2419
- MIT · © 2026 Rafael Sales. Catalogues, templates, import parsers, layout, payload formatting.{" "}
2420
- <a href="/licenses/openarchflow-LICENSE.txt" target="_blank" rel="noreferrer">Licence</a>
2421
- </dd>
2422
- <dt>AWS GenAI Solutions Architect Agent</dt>
2423
- <dd>
2424
- MIT-0. Agent composition, retry and rate limiting in the sidecar; prompt replaced with a cloud-neutral one.{" "}
2425
- <a href="/licenses/aws-architect-LICENSE.txt" target="_blank" rel="noreferrer">Licence</a>
2426
- </dd>
2427
- <dt>AWS Labs MCP servers</dt>
2428
- <dd>
2429
- Apache-2.0. Manifest only; not bundled or executed.{" "}
2430
- <a href="/licenses/aws-mcp-LICENSE.txt" target="_blank" rel="noreferrer">Licence</a>
2431
- </dd>
2432
- </dl>
2433
- <a href="/licenses/dependency-notices.txt" target="_blank" rel="noreferrer">Dependency notices</a>
2434
- <p className="modal-fine">Cloud names identify design concepts. No vendor endorsement or icon packs.</p>
2462
+ <h2>AtMem</h2><a href="/licenses/atmem-APACHE-2.0.txt" target="_blank" rel="noreferrer">Apache 2.0</a>
2435
2463
  </>
2436
2464
  ) : (
2437
2465
  <>
2438
2466
  <h2>AtMem AI Architect</h2>
2439
- <p className="muted">Version {appVersion} · local candidate</p>
2440
- <p>
2441
- Start in Design to describe your project and develop its architecture. Follow Architecture activity while it works, then choose Build this design to create and check its implementation package.
2442
- Part of atmem.ai. Runs on your computer; nothing is deployed.
2443
- </p>
2444
- <p className="muted">Keys: / add block · Esc up a level · ⌘↵ run</p>
2467
+ <p className="muted">v{appVersion}</p><InlineNote label="Keyboard shortcuts">/ Add block · Esc Board · ⌘↵ Run</InlineNote>
2445
2468
  </>
2446
2469
  )}
2447
2470
  </section>
@@ -1,11 +1,13 @@
1
+ import {verifiedReceipt} from './validation-receipt.mjs';
1
2
  import {deliveryPack} from './delivery.mjs';
2
- export async function outputBundle(project,run,environment){
3
+ export async function outputBundle(project,run,environment,checks){
3
4
  const snapshot=structuredClone(project);
4
5
  const files=deliveryPack(snapshot,run,environment);
5
6
  const {visualFiles}=await import('./visual-export.mjs');
6
7
  Object.assign(files,await visualFiles(snapshot,files['specification.md'],files['spec-kit/implementation-spec.md']));
8
+ if(checks){const terraform=Object.fromEntries(Object.entries(files).filter(([name])=>name.startsWith('terraform/')).map(([name,value])=>[name.slice(10),value]));const receipt=await verifiedReceipt(terraform,checks);if(receipt)files['terraform/validation-receipt.json']=JSON.stringify(receipt,null,2);}
7
9
  const manifest=JSON.parse(files['manifest.json']);manifest.files=Object.keys(files);
8
10
  files['manifest.json']=JSON.stringify(manifest,null,2);
9
11
  const slug=snapshot.name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'')||'atflow';
10
- return {files,manifest,filename:`${slug}-r${snapshot.revision}-delivery.zip`};
12
+ return {project,files,manifest,filename:`${slug}-r${snapshot.revision}-delivery.zip`};
11
13
  }