@atmem/architect 0.2.0-alpha.4.1 → 0.2.1

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 (66) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +21 -21
  4. package/cli/runtime.mjs +3 -1
  5. package/dist/assets/{index-qtCM2D7O.js → index-D4CZ_n8f.js} +19166 -19094
  6. package/dist/assets/index-DuveVTIx.css +1 -0
  7. package/dist/assets/{index.es-C6C0sneB.js → index.es-Fcqg-orY.js} +1 -1
  8. package/dist/assets/{visual-export-DjbjgQbA.js → visual-export-4PDDTAP7.js} +3 -3
  9. package/dist/index.html +2 -2
  10. package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
  11. package/dist/licenses/memory-dependency-notices.txt +149 -2241
  12. package/package.json +3 -3
  13. package/public/licenses/atmem-APACHE-2.0.txt +202 -0
  14. package/public/licenses/memory-dependency-notices.txt +149 -2241
  15. package/server/app.py +1 -1
  16. package/server/codex_app_server.py +5 -5
  17. package/server/codex_worker_policy.py +27 -4
  18. package/server/engineering_artifacts.py +7 -1
  19. package/server/engineering_handoff.py +9 -0
  20. package/server/engineering_routes.py +16 -2
  21. package/server/local_providers.py +7 -3
  22. package/server/memory-requirements.lock +6 -6
  23. package/server/memory-requirements.txt +2 -2
  24. package/server/memory_context.py +58 -0
  25. package/server/memory_routes.py +67 -8
  26. package/server/memory_service.py +40 -5
  27. package/server/memory_worker.py +76 -11
  28. package/server/orca_handoff.py +90 -0
  29. package/server/pricing.py +161 -1
  30. package/server/prompts.py +13 -0
  31. package/server/request_uploads.py +65 -0
  32. package/server/terraform_routes.py +47 -1
  33. package/server/windows_snapshot.py +119 -0
  34. package/src/ArchitectureWorkPanel.jsx +0 -7
  35. package/src/ArtifactChecks.jsx +2 -2
  36. package/src/ChatMessage.jsx +12 -2
  37. package/src/EngagementViews.jsx +18 -16
  38. package/src/EngineeringPanel.jsx +35 -28
  39. package/src/FileTree.jsx +7 -0
  40. package/src/FinalChecklist.jsx +23 -0
  41. package/src/HelpNote.jsx +3 -0
  42. package/src/ImplementationDelivery.jsx +56 -17
  43. package/src/JourneyViews.jsx +20 -17
  44. package/src/LocationChoices.jsx +12 -0
  45. package/src/MemoryPanel.jsx +43 -31
  46. package/src/OpenAIVoicePanel.jsx +15 -14
  47. package/src/OutputsView.jsx +28 -16
  48. package/src/ProjectChecks.jsx +58 -0
  49. package/src/StarPrompt.jsx +26 -0
  50. package/src/WorkbenchDialogs.jsx +1 -1
  51. package/src/conversation.mjs +1 -0
  52. package/src/delivery.mjs +1 -1
  53. package/src/engineering.css +10 -0
  54. package/src/estimate.mjs +5 -3
  55. package/src/final-package.mjs +34 -0
  56. package/src/journey-workspace.css +39 -0
  57. package/src/journey.mjs +12 -2
  58. package/src/large-request.mjs +25 -0
  59. package/src/main.jsx +129 -98
  60. package/src/output-bundle.mjs +18 -4
  61. package/src/pricing.mjs +59 -3
  62. package/src/project-file.mjs +1 -1
  63. package/src/useChatScroll.jsx +5 -4
  64. package/src/useEngineeringStatus.mjs +17 -0
  65. package/src/validation-receipt.mjs +1 -1
  66. package/dist/assets/index-B97MkVMs.css +0 -1
package/src/main.jsx CHANGED
@@ -1,9 +1,14 @@
1
+ import {StarPrompt,STAR_URL} from './StarPrompt.jsx';
2
+ import {prefillAcceptedSettings} from './engineering-defaults.mjs';
3
+ import {LocationChoices} from './LocationChoices.jsx';
4
+ import {InlineNote} from './HelpNote.jsx';
5
+ import {ProjectChecks} from './ProjectChecks.jsx';
6
+ import {useEngineeringStatus} from './useEngineeringStatus.mjs';
1
7
  import {ChatResizeHandle} from './ChatResizeHandle.jsx';
2
8
  import {copyProject} from './project-copy.mjs';
3
9
  import './journey-workspace.css';
4
10
  import {synchronizeEngineeringIntent,applyArchitectureChange} from './architecture-work.mjs';
5
11
  import {applyArtifactWork} from './artifact-work.mjs';
6
- import {ArchitectureWorkPanel} from './ArchitectureWorkPanel.jsx';
7
12
  import {hasArchitectureDesign} from './design-build-flow.mjs';
8
13
  import {ArchitectureTurn,EarlierArchitectureActivity} from './ArchitectureActivity.jsx';
9
14
  import {recordArchitectureTurn,finishArchitectureTurn,architectureChatEntries} from './architecture-activity.mjs';
@@ -33,7 +38,7 @@ import {OpenAIVoicePanel} from './OpenAIVoicePanel.jsx';
33
38
  import { OutputsView } from "./OutputsView.jsx";
34
39
  import { outputBundle } from "./output-bundle.mjs";
35
40
  import { TerraformView } from "./TerraformView.jsx";
36
- import { journey, advanceJourney, DESIGN_SECTIONS,journeySectionId,completedStages, designSection, journeyAction } from "./journey.mjs";
41
+ import { journey, advanceJourney, acknowledgeStage, DESIGN_SECTIONS,journeySectionId,completedStages, designSection, journeyAction } from "./journey.mjs";
37
42
  import "@xyflow/react/dist/style.css";
38
43
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
39
44
  import { createRoot } from "react-dom/client";
@@ -59,6 +64,8 @@ import {
59
64
  ChevronRight,
60
65
  GitBranch,
61
66
  Maximize2,
67
+ ChevronsLeftRight,
68
+ ChevronsRightLeft,
62
69
  MoreHorizontal,
63
70
  PanelRightClose,
64
71
  PanelRightOpen,
@@ -99,6 +106,8 @@ import {
99
106
  Eraser,
100
107
  LayoutGrid,
101
108
  ArrowLeft,
109
+ ArrowRightToLine,
110
+ ArrowRightFromLine,
102
111
  FolderOpen,
103
112
  Moon,
104
113
  Sun,
@@ -123,12 +132,12 @@ import { withDerived } from "./derive.mjs";
123
132
  import { MemoryPanel, ContextUsed } from "./MemoryPanel.jsx";
124
133
  import { defaultAssessment } from "./engagement.mjs";
125
134
  import { setUsage } from "./estimate.mjs";
126
- import { applyPriceQuotes } from "./pricing.mjs";
135
+ import { applyPriceQuotes, fetchPlanningPrices, storageNodes, lambdaNodes, googleNodes } from "./pricing.mjs";
127
136
  import { applyDecisionUpdates, applyRequirementDrafts, proposeCommentRevisions } from "./consultation.mjs";
128
137
  import { terraformSketch } from "./terraform.mjs";
129
138
  import { WORKFLOWS, conceptSignature, essentialsKnown, nextQuestion, opening, progress, sketch, understand } from "./discovery.mjs";
130
139
  import { review } from "./review.mjs";
131
- import { estimate } from "./estimate.mjs";
140
+ import { setProjectSize, estimate } from "./estimate.mjs";
132
141
  import { DeployView, EstimateView, ProjectsDialog, ReviewView, TestCard } from "./JourneyViews.jsx";
133
142
  import { newRequirement } from "./records.mjs";
134
143
  import { DiscoverView, OptionsView, DecisionsView, MeasuresEditor, RequirementsView } from "./EngagementViews.jsx";
@@ -289,7 +298,7 @@ function BlockNode({ data, selected }) {
289
298
  </span>
290
299
  <span className="blk-text">
291
300
  <strong>{data.label}</strong>
292
- {data.description && <span className="blk-why">{data.description}</span>}
301
+ {data.service&&<small className="blk-service">{data.service}</small>}
293
302
  {(data.origin === "existing" || data.provider !== "tools") && (
294
303
  <small>
295
304
  <i className="pdot" />
@@ -452,7 +461,7 @@ function Linked({ project, id, hideNeeds = false }) {
452
461
  );
453
462
  }
454
463
 
455
- function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewConfirm, onReviewDiscuss, onReviewQuestion, onReviewResolve, onRequirementSave, onWorkModelChange, onChange, onRemove, onOpen, onBack }) {
464
+ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewConfirm, onReviewDiscuss, onReviewQuestion, onReviewResolve, onRequirementSave, onWorkModelChange, onChange, onRemove, onOpen, onBack,onSpec }) {
456
465
  const trace = run?.steps.find((s) => s.id === node.id);
457
466
  const inputs = project.edges.filter((e) => e.target === node.id);
458
467
  const outputs = project.edges.filter((e) => e.source === node.id);
@@ -464,11 +473,11 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
464
473
  <section className="detail" aria-label="Block detail">
465
474
  <div className="detail-navigation">
466
475
  <button className="back" onClick={onBack} aria-label="Back to the board">
467
- <ArrowLeft size={15} /> Back to board <kbd>Esc</kbd>
476
+ <ArrowLeft size={15} /><kbd>Esc</kbd>
468
477
  </button>
469
478
  </div>
470
479
  <div className="detail-col">
471
- <h3>Inputs</h3>
480
+ <h3 aria-label="Inputs" title="Inputs"><ArrowRightToLine size={20}/></h3>
472
481
  {inputs.length ? (
473
482
  inputs.map((e) => (
474
483
  <button key={e.id} className="terminal" onClick={() => onOpen(e.source)}>
@@ -496,9 +505,8 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
496
505
  onChange={(e) => set({ label: e.target.value })}
497
506
  />
498
507
  </div>
499
- <p className="muted">{node.data.description || "No description."}</p>
508
+ <button className="link" onClick={onSpec}>Specification ↗</button>
500
509
  <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
510
  <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
511
  <details className="component-settings"><summary>Component settings</summary><div className="fields">
504
512
  <label>
@@ -590,7 +598,7 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
590
598
  </button>
591
599
  </div>
592
600
  <div className={`detail-col ${probe ? "probed" : ""}`}>
593
- <h3>Outputs</h3>
601
+ <h3 aria-label="Outputs" title="Outputs"><ArrowRightFromLine size={20}/></h3>
594
602
  {outputs.length ? (
595
603
  outputs.map((e) => (
596
604
  <button key={e.id} className="terminal" onClick={() => onOpen(e.target)}>
@@ -640,7 +648,7 @@ const welcome = (project) => ({
640
648
  role: "assistant",
641
649
  text: project.nodes.length || project.brief?.application?.trim()
642
650
  ? `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.`,
651
+ : `What would you like to build?`,
644
652
  });
645
653
 
646
654
  const THEME = "atmem-architect-theme";
@@ -654,7 +662,10 @@ function savedTheme() {
654
662
  }
655
663
  document.documentElement.dataset.theme = savedTheme();
656
664
 
665
+ 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:[]};}}
666
+ 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
667
  function App() {
668
+ const [starOffer,setStarOffer]=useState(0);
658
669
  const [project, setProject] = useState(readSaved);
659
670
  const projectRef = useRef(project);
660
671
  projectRef.current = project;
@@ -676,9 +687,9 @@ function App() {
676
687
  [level, setLevel] = useState(initialCanvasView.level),
677
688
  [focus, setFocus] = useState(initialCanvasView.focus),
678
689
  [selected, setSelected] = useState(initialCanvasView.selected),
679
- [messages, setMessages] = useState(() => conversationMessages(projectRef.current, [welcome(projectRef.current)])),
690
+ [messages, setMessagesState] = useState(() => conversationMessages(projectRef.current, [welcome(projectRef.current)])),
680
691
  [reviewView, setReviewView] = useState("findings"),
681
- [input, setInput] = useState(""),
692
+ [input, setInput] = useState(()=>{try{return localStorage.getItem('atmem-chat-draft:'+projectRef.current.id)||'';}catch{return '';}}),
682
693
  [pending, setPending] = useState(null),
683
694
  [busy, setBusy] = useState(false),
684
695
  [scenario, setScenario] = useState("happy"),
@@ -699,6 +710,19 @@ function App() {
699
710
  [probe, setProbe] = useState(null),
700
711
  [snapshot, setSnapshot] = useState(null),
701
712
  [measured, setMeasured] = useState({});
713
+ const setMessages=useCallback(updater=>setMessagesState(previous=>{
714
+ const next=typeof updater==='function'?updater(previous):updater;
715
+ if(typeof updater!=='function')return next;
716
+ 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()});
717
+ }),[]);
718
+ const draftOwner=useRef(project.id);
719
+ useEffect(()=>{
720
+ if(draftOwner.current!==project.id){draftOwner.current=project.id;try{setInput(localStorage.getItem('atmem-chat-draft:'+project.id)||'');}catch{setInput('');}return;}
721
+ try{localStorage.setItem('atmem-chat-draft:'+project.id,input);}catch{}
722
+ },[project.id,input]);
723
+ const [chatView,setChatView]=useState(()=>readChatView(project.id));
724
+ useEffect(()=>setChatView(readChatView(project.id)),[project.id]);
725
+ function changeChatView(value){setChatView(value);try{localStorage.setItem('atmem-chat-view:'+project.id,JSON.stringify(value));}catch{}}
702
726
  const messagesRef=useRef(messages);messagesRef.current=messages;
703
727
  const [session, setSession] = useState(null),
704
728
  [connection, setConnection] = useState(readConnection),
@@ -718,6 +742,37 @@ function App() {
718
742
  useEffect(()=>{saveCanvasView(project,{level,focus,selected,positions:topologyPositions});},[project.id,level,focus,selected,topologyPositions]);
719
743
  const emulation=useEmulation(project,environment,session);
720
744
  const emulationWorking=emulation.running||(agentBusy&&!!emulation.result);
745
+ const [pricingState,setPricingState]=useState({busy:false,errors:[]});
746
+ const [generationPending,setGenerationPending]=useState(false);
747
+ const [checking,setChecking]=useState(false),[checkReceipt,setCheckReceipt]=useState(null);
748
+ const engineeringStatus=useEngineeringStatus(session,project.id);
749
+ const [architectAction,setArchitectAction]=useState('Architect is working…');
750
+ const pricingRequest=useRef(0);
751
+ const pricingAttempt=useRef(new Set());
752
+ const pricingKey=pricingSignature(project);
753
+ async function refreshEstimate(selected){
754
+ if(!session?.token)return;
755
+ const resolved=prefillAcceptedSettings(selected);
756
+ const requested=resolved.estimate?.projectSize?resolved:setProjectSize(resolved,'small');
757
+ if(requested!==selected)updateEngagement(requested);
758
+ const request=++pricingRequest.current;pricingAttempt.current.add(pricingSignature(requested));
759
+ const baseline=JSON.stringify([requested.estimate?.prices,requested.estimate?.details]);
760
+ const identity=JSON.stringify([requested.id,requested.estimate?.projectSize,requested.estimate?.usage,requested.facts,requested.nodes]);
761
+ setPricingState({busy:true,errors:[]});
762
+ try{
763
+ const result=await fetchPlanningPrices(session,requested),live=projectRef.current;
764
+ if(request!==pricingRequest.current)return;
765
+ 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;}
766
+ // Merge only the fetched pricing fields; preserve concurrent design and conversation edits.
767
+ updateEngagement({...live,estimate:{...live.estimate,prices:result.project.estimate?.prices,details:result.project.estimate?.details}});
768
+ setPricingState({busy:false,errors:result.errors});
769
+ }catch(error){if(request===pricingRequest.current)setPricingState({busy:false,errors:[{reason:error.message}]});}
770
+ }
771
+ useEffect(()=>{
772
+ if(!session?.token||!project.nodes.length)return;
773
+ if(pricingAttempt.current.has(pricingKey))return;
774
+ pricingAttempt.current.add(pricingKey);void refreshEstimate(project);
775
+ },[mode,session?.token,pricingKey]);
721
776
  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
777
  function resizeChat(width){setChatWidth(width);try{localStorage.setItem('atmem-architect-chat-width',String(width));}catch{}}
723
778
  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 +1294,7 @@ function App() {
1239
1294
  setInput("");
1240
1295
  }
1241
1296
  if(origin==='chat'){setPending(null);if(!intakeOptions.alreadyLogged)setAttachments([]);}
1242
- if (origin === "chat") setChatOpen(true);
1297
+ setChatOpen(true);
1243
1298
  if (!connection) {
1244
1299
  setRequestState(requestIds,'failed');
1245
1300
  setModal("welcome");
@@ -1261,6 +1316,7 @@ function App() {
1261
1316
  const controller = new AbortController();
1262
1317
  agentAbort.current = controller;
1263
1318
  chatScroll.followLatest();
1319
+ setArchitectAction(origin==='block-review'?`Reviewing ${reviewTarget?.label||'the block'}…`:origin==='chat'?'Architect is working…':'Updating the design…');
1264
1320
  setAgentBusy(true);
1265
1321
  const activityId=session.architectureActivityEnabled?crypto.randomUUID():null;
1266
1322
  try {
@@ -1526,14 +1582,17 @@ function App() {
1526
1582
  const exportFiles = (files, suffix) => {
1527
1583
  download(zipSync(Object.fromEntries(Object.entries(files).map(([name, content]) => [name, typeof content === "string" ? strToU8(content) : content]))), `${slug}-r${project.revision}-${suffix}.zip`, "application/zip");
1528
1584
  };
1529
- const downloadBundle = bundle => {
1585
+ const downloadBundle = async bundle => {
1586
+ bundle=await(bundle.prepare?.()||bundle);
1587
+ if(bundle.project!==projectRef.current)throw Error('Design changed while preparing files. Download again.');
1530
1588
  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.");
1589
+ if(bundle.project?.id===projectRef.current.id)updateEngagement(acknowledgeStage(projectRef.current,'delivery'));
1590
+ notify("ZIP downloaded.");setStarOffer(value=>value+1);
1532
1591
  };
1533
1592
  const exportPack = async () => {
1534
1593
  try {
1535
1594
  notify("Preparing documents and diagram exports…");
1536
- downloadBundle(await outputBundle(project,run,environment));
1595
+ await downloadBundle(await outputBundle(project,run,environment));
1537
1596
  } catch(e) { notify(e.message); }
1538
1597
  };
1539
1598
  const exportTerraform = async (validation = null) => {
@@ -1764,14 +1823,28 @@ function App() {
1764
1823
  const onBoard = testing || (mode === "design" && designView === "board");
1765
1824
  const diff = pending?.next ? proposalDiff(project, pending.next) : null;
1766
1825
 
1826
+ const specificationChecks=(
1827
+ <ReviewView expanded
1828
+ project={project}
1829
+ findings={findings}
1830
+ onChange={updateEngagement}
1831
+ onOpen={(id) => {
1832
+ setMode("design");
1833
+ setDesignView("board");
1834
+ openBlock(id);
1835
+ }}
1836
+ onAddRequirement={(category) => {
1837
+ setMode("design");setDesignView("requirements");setChatOpen(true);
1838
+ 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"});
1839
+ }}
1840
+ />
1841
+ );
1767
1842
  return (
1768
1843
  <div style={{"--chat-panel-width":`${chatWidth}px`}} className={`app ${chatOpen&&!buildView ? "" : "chat-closed"} ${buildView?"build-workspace":""}`}>
1769
1844
  <header className="bar">
1770
- <button className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
1845
+ <button aria-label="About AtMem AI Architect" className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
1771
1846
  <Mark />
1772
- <span>
1773
- AtMem.ai<span className="brand-divider">|</span>AI Architect
1774
- </span>
1847
+
1775
1848
  </button>
1776
1849
  <nav className="crumbs" aria-label="Level"><span className="workspace-label">PROJECT</span>
1777
1850
  <button className="projects-btn" aria-label="Projects" title="Your projects" onClick={() => (refreshProjects(), setModal("projects"))}>
@@ -1815,18 +1888,19 @@ function App() {
1815
1888
  <button role="menuitem" onClick={() => setModal("templates")} disabled={busy}><LayoutTemplate size={16}/>Templates</button>
1816
1889
  <hr/><button role="menuitem" onClick={() => setModal("import")} disabled={busy}><GitBranch size={16}/>Import diagram</button>
1817
1890
  <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>
1891
+ <hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><BrainActivity size={18} active={false}/>AtMem memory</button>
1819
1892
  <button role="menuitem" onClick={() => setModal("start-over")} disabled={busy || agentBusy}><RotateCcw size={16}/>Start over</button>
1820
1893
  <hr />
1821
- <button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>Connections</button>
1894
+ <button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>AI providers</button>
1822
1895
  <button role="menuitem" onClick={() => setModal("licenses")}><ShieldCheck size={16}/>Licences</button>
1896
+ <a role="menuitem" href={STAR_URL} target="_blank" rel="noopener noreferrer">Star on GitHub ↗</a>
1823
1897
  </div>
1824
1898
  )}
1825
1899
  </header>
1826
1900
 
1827
1901
  <section className="journey-wizard unified-journey" aria-label="Architecture journey">
1828
1902
  <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>}
1903
+ {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
1904
  </section>
1831
1905
  <main
1832
1906
  id="canvas"
@@ -1846,7 +1920,7 @@ function App() {
1846
1920
  {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
1921
  {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
1922
  {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>}
1923
+ {!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
1924
  <ReactFlow
1851
1925
  nodes={canvasNodes}
1852
1926
  edges={canvasEdges}
@@ -2017,6 +2091,7 @@ function App() {
2017
2091
  onWorkModelChange={changeArchitectureWork}
2018
2092
  onRemove={() => removeNode(selectedNode.id)}
2019
2093
  onOpen={(id) => openBlock(id)}
2094
+ onSpec={()=>{visitJourney('scope');setTimeout(()=>document.getElementById('component-spec-'+selectedNode.id)?.scrollIntoView({block:'start'}),0);}}
2020
2095
  onBack={goUp}
2021
2096
  />
2022
2097
  )}
@@ -2093,31 +2168,15 @@ function App() {
2093
2168
 
2094
2169
  </div>
2095
2170
  <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>}
2171
+ <header className="review-intro"><h2>Review</h2></header>
2172
+ <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>
2173
+ <section className="review-costs" aria-label="Project costs">
2174
+ {["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'});}}} />}
2175
+ </section>
2176
+ {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
2177
  {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>}
2178
+ {mode==='review'&&<ProjectChecks generationPending={generationPending} receipt={checkReceipt} project={project} environment={environment} session={session} onReceipt={setCheckReceipt} onBusy={setChecking}/>}
2179
+ {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>}
2121
2180
  </div>
2122
2181
 
2123
2182
 
@@ -2125,16 +2184,16 @@ function App() {
2125
2184
 
2126
2185
  {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
2186
 
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")} />}
2187
+ {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")} />}
2129
2188
  {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
2189
  <div className="choices-workspace" hidden={!(mode==="design"&&["options","decisions","location"].includes(designView))}>
2131
2190
  {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"); }} />
2191
+ <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
2192
  )}
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>}
2193
+ {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
2194
  </div>
2136
2195
  {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"); }} />
2196
+ <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
2197
  )}
2139
2198
  </main>
2140
2199
 
@@ -2142,9 +2201,9 @@ function App() {
2142
2201
  {chatOpen&&!buildView && (
2143
2202
  <aside className="chat" aria-label="Chat"><ChatResizeHandle width={chatWidth} onChange={resizeChat}/>
2144
2203
  <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>
2204
+ <div className="chat-heading-row"><strong className="chat-title" aria-label="AI Architect">
2205
+ <Mark size={24} />
2206
+ </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
2207
  <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
2208
  <input
2150
2209
  type="checkbox"
@@ -2158,13 +2217,13 @@ function App() {
2158
2217
  />
2159
2218
  Draw as we talk
2160
2219
  </label>
2161
- <button className="model" onClick={() => setModal("connections")} title="Connections">
2220
+ <button className="model" onClick={() => setModal("connections")} title="AI providers">
2162
2221
  <span className={`led ${connection && session ? "on" : ""}`} />
2163
2222
  {connection ? `${connection.model}${connection.effort ? ` · ${EFFORT_LABEL[connection.effort] || connection.effort}` : ""}` : "Connect a model"}
2164
2223
  </button>
2165
2224
  </div>
2166
2225
  <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>
2226
+ <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
2227
  <button
2169
2228
  className={`known-bar ${peek === "known" ? "on" : ""}`}
2170
2229
  aria-label="Design context" aria-expanded={peek === "known"} title="Review what is known, assumed or still open"
@@ -2207,14 +2266,14 @@ function App() {
2207
2266
  )}
2208
2267
  <div className="messages" ref={chatScroll.viewport}>
2209
2268
  <div className="message-list" ref={chatScroll.content}>
2210
- {architectureChatEntries(project,messages).map(entry=>{
2269
+ {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
2270
  if(entry.kind==='activity-error')return <p key="activity-error" role="alert">{entry.error}</p>;
2212
2271
  if(entry.kind==='legacy-activity')return <EarlierArchitectureActivity key={entry.id} entries={entry.entries} projectId={project.id} session={session}/>;
2213
2272
  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;
2273
+ 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
2274
  return (
2216
2275
  <div key={i} className={`msg ${m.role}`}>
2217
- <ChatMessage text={m.text}/>
2276
+ <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
2277
  {(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
2278
  {!!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
2279
  {m.fix && (
@@ -2260,15 +2319,9 @@ function App() {
2260
2319
  )}
2261
2320
  </div>
2262
2321
  </div>
2322
+ {chatScroll.away&&<button className="chat-jump-latest" aria-label="Jump to latest message" onClick={chatScroll.followLatest}>↓</button>}
2263
2323
  {!!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
2324
  <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
2325
  {mode === "design" && project.workflow && !project.nodes.length && !pending && (
2273
2326
  <button className="primary-chip" disabled={busy || agentBusy} onClick={sketchIt}>Sketch it →</button>
2274
2327
  )}
@@ -2278,6 +2331,7 @@ function App() {
2278
2331
  </button>
2279
2332
  ))}
2280
2333
  </div>
2334
+ {(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?'Generating project files…':['starting','running'].includes(engineeringStatus?.state)?'Generating project files…':checking?'Running checks…':'Calculating costs…'}</span></div>}
2281
2335
  {readingAttachments&&<p className="attachment-status" role="status"><BrainActivity/> Reading files…</p>}
2282
2336
  {attachmentError&&<p className="attachment-status" role="alert">{attachmentError}</p>}
2283
2337
  {!!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>}
@@ -2340,13 +2394,14 @@ function App() {
2340
2394
  {toast === "Preparing documents and diagram exports…"&&<BrainActivity/>}{toast}
2341
2395
  </div>
2342
2396
  )}
2397
+ <StarPrompt offer={starOffer}/>
2343
2398
  {modal && (
2344
2399
  <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}>
2400
+ <section className={`modal ${modal==="memory"?"memory-modal":""}`} role="dialog" aria-modal="true" aria-label={modal}>
2346
2401
  {!(modal === "welcome" && !connection) && <button className="icon modal-close" onClick={() => setModal(null)} aria-label="Close dialog">
2347
2402
  <X size={16} />
2348
2403
  </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" ? (
2404
+ {modal === "project-file" ? (<><h2>Open project</h2><button className="btn primary" onClick={()=>{fileImportAsCopy.current=false;fileRef.current.click();}}>Open project</button></>) : modal === "memory" ? (
2350
2405
  <MemoryPanel key={project.id} session={session} project={project} onError={notify} onStatus={status=>setMemorySync({projectId:project.id,...status})} />
2351
2406
  ) : modal === "templates" ? (
2352
2407
  <TemplateDialog onLoad={loadDesign} />
@@ -2362,7 +2417,7 @@ function App() {
2362
2417
  environment={environment}
2363
2418
  />
2364
2419
  ) : 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
2420
+ <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
2421
  list={saved}
2367
2422
  folder={saved.folder}
2368
2423
  currentId={project.id}
@@ -2412,36 +2467,12 @@ function App() {
2412
2467
  />
2413
2468
  ) : modal === "licenses" ? (
2414
2469
  <>
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>
2470
+ <h2>AtMem</h2><a href="/licenses/atmem-APACHE-2.0.txt" target="_blank" rel="noreferrer">Apache 2.0</a>
2435
2471
  </>
2436
2472
  ) : (
2437
2473
  <>
2438
2474
  <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>
2475
+ <p className="muted">v{appVersion}</p><InlineNote label="Keyboard shortcuts">/ Add block · Esc Board · ⌘↵ Run</InlineNote>
2445
2476
  </>
2446
2477
  )}
2447
2478
  </section>
@@ -1,11 +1,25 @@
1
+ import {verifiedReceipt} from './validation-receipt.mjs';
1
2
  import {deliveryPack} from './delivery.mjs';
2
- export async function outputBundle(project,run,environment){
3
+ export const VISUAL_PATHS=['spec-kit/implementation-spec.pdf','specification.pdf','diagram.svg','diagram.png','diagram.pdf'];
4
+ export async function outputBundle(project,run,environment,checks,{deferVisuals=false}={}){
3
5
  const snapshot=structuredClone(project);
4
6
  const files=deliveryPack(snapshot,run,environment);
5
- const {visualFiles}=await import('./visual-export.mjs');
6
- Object.assign(files,await visualFiles(snapshot,files['specification.md'],files['spec-kit/implementation-spec.md']));
7
+ 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
8
  const manifest=JSON.parse(files['manifest.json']);manifest.files=Object.keys(files);
8
9
  files['manifest.json']=JSON.stringify(manifest,null,2);
9
10
  const slug=snapshot.name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'')||'atflow';
10
- return {files,manifest,filename:`${slug}-r${snapshot.revision}-delivery.zip`};
11
+ const base={project,files,manifest,filename:`${slug}-r${snapshot.revision}-delivery.zip`,visualPaths:VISUAL_PATHS};
12
+ let pending;
13
+ async function prepare(){
14
+ if(!pending)pending=(async()=>{
15
+ const {visualFiles}=await import('./visual-export.mjs');
16
+ const fullFiles={...files,...await visualFiles(snapshot,files['specification.md'],files['spec-kit/implementation-spec.md'])};
17
+ const fullManifest={...manifest,files:Object.keys(fullFiles)};
18
+ fullFiles['manifest.json']=JSON.stringify(fullManifest,null,2);
19
+ const full={...base,files:fullFiles,manifest:fullManifest,visualPaths:[]};
20
+ return {...full,prepare:()=>Promise.resolve(full)};
21
+ })().catch(error=>{pending=null;throw error;});
22
+ return pending;
23
+ }
24
+ return deferVisuals?{...base,prepare}:prepare();
11
25
  }