@echomem/mcp 1.4.7 → 1.4.8

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.
@@ -125,12 +125,41 @@ export function renderSetupPage() {
125
125
  body.cityMode header {
126
126
  display: none;
127
127
  }
128
+ body.extractMode header {
129
+ display: none;
130
+ }
131
+ body.readyMode header {
132
+ display: none;
133
+ }
134
+ body.extractMode main {
135
+ min-height: 100vh;
136
+ display: grid;
137
+ align-items: center;
138
+ justify-items: center;
139
+ }
140
+ body.readyMode main {
141
+ min-height: 100vh;
142
+ display: grid;
143
+ align-items: center;
144
+ justify-items: center;
145
+ padding: clamp(24px, 5vw, 56px);
146
+ }
128
147
  body.cityMode #app {
129
148
  display: block;
130
149
  min-height: 100vh;
131
150
  color: var(--echo-ink-text);
132
151
  text-align: left;
133
152
  }
153
+ body.extractMode #app {
154
+ width: 100%;
155
+ display: grid;
156
+ justify-items: center;
157
+ }
158
+ body.readyMode #app {
159
+ width: 100%;
160
+ display: grid;
161
+ justify-items: center;
162
+ }
134
163
 
135
164
  /* clean, consistent loader for the post-connect "working/starting" states */
136
165
  .miniLoad { display: grid; gap: 12px; max-width: 520px; padding: clamp(24px, 6vw, 64px) 0; }
@@ -689,21 +718,651 @@ export function renderSetupPage() {
689
718
  }
690
719
  /* extraction dashboard — plate layout */
691
720
  .exHero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 0.95fr); gap: clamp(20px, 3vw, 40px); align-items: center; margin-top: 8px; }
692
- .exCopy h2 { max-width: 560px; margin: 8px 0 0; font-size: clamp(30px, 4vw, 48px); line-height: 1.03; }
693
- .exCopy .exSub { margin: 14px 0 0; max-width: 520px; font-size: clamp(15px, 1.3vw, 17px); line-height: 1.55; color: var(--echo-ink-mute); }
721
+ /* Centered composition: mascot on top, then everything stacked and centered on one axis. */
722
+ .readyDecision {
723
+ width: 100%;
724
+ max-width: min(720px, calc(100vw - clamp(32px, 8vw, 120px)));
725
+ display: grid;
726
+ justify-items: center;
727
+ text-align: center;
728
+ gap: 20px;
729
+ margin: 0 auto;
730
+ }
731
+ .readyHero {
732
+ display: grid;
733
+ justify-items: center;
734
+ gap: 16px;
735
+ margin: 0 auto;
736
+ }
737
+ .readyMascot {
738
+ width: 84px;
739
+ height: 84px;
740
+ object-fit: contain;
741
+ filter: drop-shadow(0 12px 18px rgba(26,58,143,0.18));
742
+ }
743
+ .readyHero .exCopy h2 {
744
+ margin: 0;
745
+ font-size: clamp(32px, 4.6vw, 60px);
746
+ line-height: 0.98;
747
+ }
748
+ .readyHero .exCopy .exSub {
749
+ max-width: 560px;
750
+ margin: 12px auto 0;
751
+ font-size: clamp(16px, 1.45vw, 18px);
752
+ }
753
+ .readyAction {
754
+ display: grid;
755
+ grid-template-columns: minmax(0, 1fr) minmax(210px, auto);
756
+ gap: clamp(18px, 3vw, 34px);
757
+ align-items: end;
758
+ margin-top: 2px;
759
+ padding: 22px 0 24px;
760
+ border-top: 1px solid var(--echo-line-soft);
761
+ border-bottom: 1px solid var(--echo-line-soft);
762
+ }
763
+ .readyRun {
764
+ display: grid;
765
+ grid-template-columns: minmax(220px, 0.8fr) minmax(280px, 1.2fr);
766
+ gap: clamp(18px, 3vw, 34px);
767
+ align-items: start;
768
+ }
769
+ .exHeroReady {
770
+ width: 100%;
771
+ max-width: min(860px, calc(100vw - clamp(36px, 9vw, 140px)));
772
+ grid-template-columns: minmax(0, 1fr);
773
+ justify-content: center;
774
+ justify-items: stretch;
775
+ margin: clamp(8px, 2vh, 24px) auto 0;
776
+ }
777
+ .exHeroReady .exCopy { width: 100%; }
778
+ .exHeroResult {
779
+ width: 100%;
780
+ max-width: min(920px, calc(100vw - clamp(36px, 8vw, 120px)));
781
+ grid-template-columns: minmax(0, 760px);
782
+ justify-content: center;
783
+ justify-items: stretch;
784
+ margin: 0 auto;
785
+ }
786
+ .exHeroResult .exPlate { display: none; }
787
+ .exHeroResult .exCopy { width: 100%; margin: 0 auto; }
788
+ .exCopy h2 { max-width: 620px; margin: 8px 0 0; font-size: clamp(34px, 4.8vw, 58px); line-height: 1.01; }
789
+ .exCopy .exSub { margin: 14px 0 0; max-width: 590px; font-size: clamp(15px, 1.3vw, 17px); line-height: 1.6; color: var(--echo-ink-mute); }
694
790
  .exLegend { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 16px; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
695
791
  .exLegend .lgDot { width: 12px; height: 12px; border-radius: 3px; display: inline-block; margin-left: 10px; }
696
792
  .exLegend .lgDot:first-child { margin-left: 0; }
697
793
  .lgNew { background: #2f6df0; }
698
794
  .lgDim { background: #cfc8b8; }
699
795
  .exHero .actions { margin-top: 22px; }
700
- .exHint { margin: 14px 0 0; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-faint); }
796
+ .readyDecision .actions {
797
+ justify-content: center;
798
+ margin-top: 0;
799
+ }
800
+ .readyDecision .actions .primary {
801
+ min-width: 240px;
802
+ min-height: 60px;
803
+ padding: 0 30px;
804
+ font-size: 16px;
805
+ box-shadow: 0 16px 28px -20px rgba(26,58,143,0.55);
806
+ }
807
+ /* Proof strip: one quiet line of evidence (source split), not stat cards. Centered. */
808
+ .proofStrip {
809
+ display: flex;
810
+ flex-wrap: wrap;
811
+ justify-content: center;
812
+ align-items: center;
813
+ gap: 8px 12px;
814
+ min-height: 26px;
815
+ margin-top: 2px;
816
+ font-family: var(--echo-font-mono);
817
+ font-size: 13px;
818
+ color: var(--echo-ink-mute);
819
+ }
820
+ .proofStrip .pf { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
821
+ .proofStrip .pf strong { color: var(--echo-ink-primary); font-weight: 800; font-variant-numeric: tabular-nums; }
822
+ .proofStrip .pfIcon { position: relative; width: 22px; height: 22px; border-radius: 6px; overflow: hidden; background: var(--echo-ink-primary); display: grid; place-items: center; flex: 0 0 auto; }
823
+ .proofStrip .pfIcon img { width: 22px; height: 22px; object-fit: cover; display: block; }
824
+ .proofStrip .pfFallback { display: none; place-items: center; width: 22px; height: 22px; color: #fff; font-size: 8px; font-weight: 900; }
825
+ .proofStrip .pfDot { color: var(--echo-ink-faint); }
826
+ .proofStrip .pfNote { color: var(--echo-ink-faint); font-size: 12px; }
827
+ /* The action block: button first, reassurances attached right under it. Centered. */
828
+ .readyGo { display: grid; gap: 11px; margin-top: 6px; justify-items: center; }
829
+ .ctaMeta { margin: 0; max-width: 480px; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 4px 9px; font-family: var(--echo-font-mono); font-size: 12px; line-height: 1.5; color: var(--echo-ink-mute); }
830
+ .ctaMeta .cm { white-space: nowrap; }
831
+ .ctaMeta .cmDot { color: var(--echo-ink-faint); }
832
+ .exHint { margin: 0; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-faint); }
833
+ .readyFoot {
834
+ display: grid;
835
+ justify-items: center;
836
+ gap: 14px;
837
+ margin-top: clamp(24px, 5vh, 52px);
838
+ opacity: 0.82;
839
+ }
840
+ .readyTime {
841
+ width: 100%;
842
+ display: grid;
843
+ gap: 7px;
844
+ align-content: start;
845
+ padding: 0;
846
+ border: 0;
847
+ background: transparent;
848
+ box-shadow: none;
849
+ }
850
+ .readyTime span {
851
+ font-family: var(--echo-font-mono);
852
+ font-size: 11px;
853
+ font-weight: 800;
854
+ letter-spacing: 0.08em;
855
+ text-transform: uppercase;
856
+ color: var(--echo-ink-mute);
857
+ }
858
+ .readyTime strong {
859
+ font-family: var(--echo-font-brand);
860
+ font-size: clamp(36px, 4vw, 54px);
861
+ font-weight: 900;
862
+ line-height: 0.92;
863
+ color: var(--echo-ink-primary);
864
+ }
865
+ .readyTime em {
866
+ max-width: 390px;
867
+ font-style: normal;
868
+ color: var(--echo-ink-mute);
869
+ font-size: 13px;
870
+ line-height: 1.45;
871
+ }
872
+ .sourceGrid {
873
+ display: grid;
874
+ gap: 8px;
875
+ margin-top: 8px;
876
+ }
877
+ .sourceHeading {
878
+ margin: 0;
879
+ font-family: var(--echo-font-mono);
880
+ font-size: 11px;
881
+ font-weight: 800;
882
+ letter-spacing: 0.08em;
883
+ text-transform: uppercase;
884
+ color: var(--echo-ink-mute);
885
+ }
886
+ .sourceCard {
887
+ display: grid;
888
+ grid-template-columns: 30px minmax(0, 1fr);
889
+ align-items: center;
890
+ gap: 10px;
891
+ border: 1px solid var(--echo-line-soft);
892
+ border-radius: 8px;
893
+ background: var(--echo-paper-soft);
894
+ padding: 8px 11px 8px 8px;
895
+ }
896
+ .sourceIcon {
897
+ width: 30px;
898
+ height: 30px;
899
+ border-radius: 7px;
900
+ overflow: hidden;
901
+ background: var(--echo-ink-primary);
902
+ display: grid;
903
+ place-items: center;
904
+ flex: 0 0 auto;
905
+ }
906
+ .sourceIcon img { width: 30px; height: 30px; object-fit: cover; display: block; }
907
+ .sourceFallback {
908
+ display: none;
909
+ color: #fff;
910
+ font-family: var(--echo-font-mono);
911
+ font-size: 9px;
912
+ font-weight: 900;
913
+ }
914
+ .sourceCard > div {
915
+ min-width: 0;
916
+ display: grid;
917
+ grid-template-columns: auto minmax(0, 1fr);
918
+ align-items: baseline;
919
+ gap: 2px 7px;
920
+ }
921
+ .sourceCard strong {
922
+ font-family: var(--echo-font-brand);
923
+ font-size: 24px;
924
+ font-weight: 900;
925
+ line-height: 1;
926
+ color: var(--echo-ink-primary);
927
+ font-variant-numeric: tabular-nums;
928
+ }
929
+ .sourceCard span {
930
+ font-family: var(--echo-font-mono);
931
+ font-size: 11px;
932
+ font-weight: 800;
933
+ letter-spacing: 0.04em;
934
+ color: var(--echo-ink-mute);
935
+ }
936
+ .sourceCard small {
937
+ display: block;
938
+ grid-column: 1 / -1;
939
+ color: var(--echo-ink-faint);
940
+ font-size: 12px;
941
+ line-height: 1.35;
942
+ }
943
+ .readyExplain {
944
+ max-width: 760px;
945
+ border-top: 1px solid var(--echo-line-soft);
946
+ padding-top: 12px;
947
+ color: var(--echo-ink-mute);
948
+ }
949
+ .readyExplain summary {
950
+ cursor: pointer;
951
+ font-family: var(--echo-font-mono);
952
+ font-size: 11px;
953
+ font-weight: 800;
954
+ letter-spacing: 0.08em;
955
+ text-transform: uppercase;
956
+ color: var(--echo-ink-primary);
957
+ }
958
+ .readyExplain summary::marker { color: var(--echo-ink-faint); }
959
+ .readyExplain p {
960
+ margin: 7px 0 0;
961
+ max-width: 640px;
962
+ color: var(--echo-ink-mute);
963
+ font-size: 14px;
964
+ line-height: 1.55;
965
+ }
966
+ .readySteps {
967
+ display: grid;
968
+ grid-template-columns: repeat(3, minmax(0, 1fr));
969
+ gap: 8px;
970
+ margin-top: 11px;
971
+ }
972
+ .readyStep {
973
+ border: 1px solid var(--echo-line-soft);
974
+ border-radius: 8px;
975
+ background: transparent;
976
+ padding: 10px 11px;
977
+ }
978
+ .readyStep strong {
979
+ display: block;
980
+ font-family: var(--echo-font-mono);
981
+ font-size: 10px;
982
+ font-weight: 800;
983
+ letter-spacing: 0.06em;
984
+ text-transform: uppercase;
985
+ color: var(--echo-ink-primary);
986
+ }
987
+ .readyStep span {
988
+ display: block;
989
+ margin-top: 5px;
990
+ color: var(--echo-ink-faint);
991
+ font-size: 12px;
992
+ line-height: 1.35;
993
+ }
701
994
  .exPlate { width: 100%; }
702
995
  /* WebGL clay city (echo-extraction-plate.html) — the only plate; no SVG layer */
703
996
  .plateStack { position: relative; width: 100%; aspect-ratio: 640 / 470; }
704
997
  .plateStack .plateGl { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: transparent; }
705
- .recon { margin-top: 16px; display: inline-block; font-family: var(--echo-font-mono); font-size: 13px; font-weight: 700; color: var(--echo-ink-primary); background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 8px; padding: 8px 12px; }
706
- @media (max-width: 820px) { .exHero { grid-template-columns: 1fr; } }
998
+ .exHeroActive {
999
+ grid-template-columns: minmax(0, 640px);
1000
+ justify-content: center;
1001
+ justify-items: stretch;
1002
+ width: 100%;
1003
+ max-width: min(920px, calc(100vw - clamp(36px, 8vw, 120px)));
1004
+ margin: 0 auto;
1005
+ }
1006
+ .exHeroActive .exPlate { display: none; }
1007
+ .exHeroActive .exCopy { width: 100%; }
1008
+ .workbench { display: grid; gap: 14px; max-width: 640px; }
1009
+ .workbench h2 {
1010
+ max-width: 520px;
1011
+ font-size: clamp(24px, 3vw, 34px);
1012
+ line-height: 1.04;
1013
+ }
1014
+ .exMeter {
1015
+ display: flex;
1016
+ align-items: baseline;
1017
+ justify-content: space-between;
1018
+ gap: 14px;
1019
+ margin-top: 10px;
1020
+ padding-bottom: 8px;
1021
+ border-bottom: 1px solid var(--echo-line-soft);
1022
+ }
1023
+ .exMeter strong {
1024
+ font-family: var(--echo-font-brand);
1025
+ font-size: clamp(38px, 5vw, 66px);
1026
+ font-weight: 900;
1027
+ line-height: 0.9;
1028
+ color: var(--echo-ink-primary);
1029
+ font-variant-numeric: tabular-nums;
1030
+ }
1031
+ .exMeter span {
1032
+ font-family: var(--echo-font-mono);
1033
+ font-size: 11px;
1034
+ font-weight: 800;
1035
+ letter-spacing: 0.07em;
1036
+ text-transform: uppercase;
1037
+ color: var(--echo-ink-mute);
1038
+ }
1039
+ .exCurrent {
1040
+ display: grid;
1041
+ grid-template-columns: auto 1fr;
1042
+ gap: 10px;
1043
+ align-items: center;
1044
+ margin-top: 2px;
1045
+ padding: 11px 0;
1046
+ border-top: 1px solid var(--echo-line-soft);
1047
+ border-bottom: 1px solid var(--echo-line-soft);
1048
+ }
1049
+ .exCurrent i {
1050
+ width: 9px;
1051
+ height: 9px;
1052
+ border-radius: 999px;
1053
+ background: var(--echo-ink-accent);
1054
+ box-shadow: 0 0 0 4px rgba(78,205,196,0.15);
1055
+ }
1056
+ .exCurrent span {
1057
+ font-family: var(--echo-font-mono);
1058
+ font-size: 11px;
1059
+ font-weight: 800;
1060
+ letter-spacing: 0.07em;
1061
+ text-transform: uppercase;
1062
+ color: var(--echo-ink-primary);
1063
+ }
1064
+ .exCurrent strong {
1065
+ display: block;
1066
+ margin-top: 3px;
1067
+ color: var(--echo-ink-text);
1068
+ font-size: 14px;
1069
+ line-height: 1.45;
1070
+ font-weight: 700;
1071
+ }
1072
+ .extractFacts {
1073
+ margin: 0;
1074
+ font-family: var(--echo-font-mono);
1075
+ font-size: 12px;
1076
+ line-height: 1.5;
1077
+ color: var(--echo-ink-mute);
1078
+ }
1079
+ .extractFacts strong {
1080
+ color: var(--echo-ink-primary);
1081
+ font-weight: 900;
1082
+ }
1083
+ .extractEndRow {
1084
+ display: flex;
1085
+ align-items: center;
1086
+ gap: 10px;
1087
+ flex-wrap: wrap;
1088
+ margin-top: 2px;
1089
+ }
1090
+ .extractEndRow button {
1091
+ min-height: 0;
1092
+ border: 0;
1093
+ border-radius: 0;
1094
+ background: transparent;
1095
+ color: var(--echo-ink-primary);
1096
+ padding: 0;
1097
+ font-family: var(--echo-font-mono);
1098
+ font-size: 12px;
1099
+ font-weight: 800;
1100
+ letter-spacing: 0.04em;
1101
+ text-transform: none;
1102
+ }
1103
+ .extractEndRow button:hover {
1104
+ text-decoration: underline;
1105
+ }
1106
+ .extractEndRow span {
1107
+ color: var(--echo-ink-faint);
1108
+ font-size: 12px;
1109
+ line-height: 1.45;
1110
+ }
1111
+ .skipStage {
1112
+ display: grid;
1113
+ gap: 14px;
1114
+ max-width: 680px;
1115
+ margin: 0 auto;
1116
+ text-align: center;
1117
+ justify-items: center;
1118
+ }
1119
+ .skipStage h2 {
1120
+ max-width: 620px;
1121
+ font-size: clamp(38px, 5vw, 64px);
1122
+ line-height: 0.98;
1123
+ }
1124
+ .skipStage p {
1125
+ max-width: 560px;
1126
+ margin: 0;
1127
+ color: var(--echo-ink-mute);
1128
+ font-size: 16px;
1129
+ line-height: 1.6;
1130
+ }
1131
+ .skipStage .seal {
1132
+ width: fit-content;
1133
+ border: 1px solid var(--echo-line-soft);
1134
+ border-radius: 999px;
1135
+ background: var(--echo-paper-white);
1136
+ padding: 8px 13px;
1137
+ font-family: var(--echo-font-mono);
1138
+ font-size: 11px;
1139
+ font-weight: 800;
1140
+ letter-spacing: 0.07em;
1141
+ text-transform: uppercase;
1142
+ color: var(--echo-ink-primary);
1143
+ }
1144
+ .resultStage { display: grid; gap: 18px; max-width: 760px; padding: 8px 0 2px; }
1145
+ .resultStage .kicker-mono { color: var(--echo-ink-seal); }
1146
+ .resultTop {
1147
+ display: grid;
1148
+ gap: 12px;
1149
+ align-items: start;
1150
+ }
1151
+ .resultTop h2 { margin-top: 6px; font-size: clamp(36px, 5vw, 58px); line-height: 0.98; }
1152
+ .resultNumber {
1153
+ width: fit-content;
1154
+ display: inline-flex;
1155
+ align-items: baseline;
1156
+ gap: 8px;
1157
+ border: 1px solid var(--echo-line-soft);
1158
+ border-radius: 999px;
1159
+ background: var(--echo-paper-white);
1160
+ padding: 7px 12px;
1161
+ }
1162
+ .resultNumber strong {
1163
+ font-family: var(--echo-font-brand);
1164
+ font-size: 18px;
1165
+ font-weight: 900;
1166
+ line-height: 1;
1167
+ color: var(--echo-ink-primary);
1168
+ font-variant-numeric: tabular-nums;
1169
+ }
1170
+ .resultNumber span {
1171
+ font-family: var(--echo-font-mono);
1172
+ font-size: 10px;
1173
+ font-weight: 800;
1174
+ letter-spacing: 0.07em;
1175
+ text-transform: uppercase;
1176
+ color: var(--echo-ink-mute);
1177
+ text-align: right;
1178
+ }
1179
+ .memoryReceipt {
1180
+ display: grid;
1181
+ gap: 10px;
1182
+ max-width: 700px;
1183
+ padding-top: 2px;
1184
+ }
1185
+ .memoryReceipt .receiptK {
1186
+ margin: 0;
1187
+ font-family: var(--echo-font-mono);
1188
+ font-size: 10px;
1189
+ font-weight: 800;
1190
+ letter-spacing: 0.08em;
1191
+ text-transform: uppercase;
1192
+ color: var(--echo-ink-mute);
1193
+ }
1194
+ .finishStats {
1195
+ display: flex;
1196
+ flex-wrap: wrap;
1197
+ gap: 6px 18px;
1198
+ }
1199
+ .finishStat {
1200
+ display: inline-flex;
1201
+ align-items: baseline;
1202
+ gap: 6px;
1203
+ border: 0;
1204
+ background: transparent;
1205
+ padding: 0;
1206
+ }
1207
+ .finishStat strong {
1208
+ font-family: var(--echo-font-brand);
1209
+ font-size: 17px;
1210
+ font-weight: 900;
1211
+ line-height: 1;
1212
+ color: var(--echo-ink-primary);
1213
+ font-variant-numeric: tabular-nums;
1214
+ }
1215
+ .finishStat span {
1216
+ font-family: var(--echo-font-mono);
1217
+ font-size: 10px;
1218
+ font-weight: 800;
1219
+ letter-spacing: 0.07em;
1220
+ text-transform: uppercase;
1221
+ color: var(--echo-ink-mute);
1222
+ }
1223
+ .retryNote {
1224
+ margin: 0;
1225
+ max-width: 640px;
1226
+ font-size: 12px;
1227
+ line-height: 1.5;
1228
+ color: var(--echo-ink-faint);
1229
+ }
1230
+ .recon { margin-top: 0; display: inline-block; font-family: var(--echo-font-mono); font-size: 13px; font-weight: 700; color: var(--echo-ink-primary); background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 8px; padding: 8px 12px; }
1231
+ .exCountdown { margin: 0; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
1232
+ .exCountdown strong { color: var(--echo-ink-primary); font-weight: 700; }
1233
+ .exOverdue { margin: 0; max-width: 620px; border: 1px solid rgba(196, 148, 42, 0.45); background: rgba(233, 185, 73, 0.12); border-radius: 8px; padding: 12px 14px; font-size: 13px; line-height: 1.5; color: var(--echo-ink-text); }
1234
+ .exOverdue strong { display: block; margin-bottom: 4px; color: var(--echo-ink-primary); }
1235
+ .tryCard { margin-top: 2px; max-width: 760px; border: 2px solid var(--echo-ink-primary); border-radius: 8px; background: var(--echo-paper-white); padding: clamp(18px, 3vw, 26px); box-shadow: 0 18px 38px -28px rgba(26,58,143,0.45); }
1236
+ .tryCard .tryK { margin: 0 0 10px; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-primary); }
1237
+ .tryCard .tryTitle {
1238
+ margin: 0 0 12px;
1239
+ font-family: var(--echo-font-brand);
1240
+ font-size: clamp(18px, 1.9vw, 23px);
1241
+ line-height: 1.15;
1242
+ font-weight: 900;
1243
+ color: var(--echo-ink-primary);
1244
+ }
1245
+ .tryCard pre { margin: 0; max-height: 72px; overflow: hidden; white-space: pre-wrap; word-break: break-word; font-family: var(--echo-font-mono); font-size: 12px; line-height: 1.5; color: var(--echo-ink-mute); background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 8px; padding: 11px 13px; -webkit-mask-image: linear-gradient(to bottom, #000 52%, transparent); mask-image: linear-gradient(to bottom, #000 52%, transparent); }
1246
+ .agentLaunch {
1247
+ display: grid;
1248
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1249
+ gap: 10px;
1250
+ margin-top: 18px;
1251
+ }
1252
+ .agentLaunch button {
1253
+ min-height: 72px;
1254
+ justify-content: flex-start;
1255
+ border-color: var(--echo-line-soft);
1256
+ background: var(--echo-paper-white);
1257
+ color: var(--echo-ink-primary);
1258
+ padding: 10px 14px;
1259
+ text-align: left;
1260
+ }
1261
+ .agentLaunch button:hover { border-color: var(--echo-ink-primary); }
1262
+ .agentIcon {
1263
+ width: 46px;
1264
+ height: 46px;
1265
+ display: grid;
1266
+ place-items: center;
1267
+ flex: 0 0 auto;
1268
+ border-radius: 11px;
1269
+ background: var(--echo-ink-primary);
1270
+ color: #fff;
1271
+ font-family: var(--echo-font-mono);
1272
+ font-size: 11px;
1273
+ font-weight: 900;
1274
+ letter-spacing: 0;
1275
+ overflow: hidden;
1276
+ box-shadow: 0 8px 16px -10px rgba(20,20,20,0.45);
1277
+ }
1278
+ .agentIcon img {
1279
+ width: 100%;
1280
+ height: 100%;
1281
+ object-fit: cover;
1282
+ display: block;
1283
+ }
1284
+ .agentIcon .agentFallback {
1285
+ width: 100%;
1286
+ height: 100%;
1287
+ display: none;
1288
+ place-items: center;
1289
+ }
1290
+ .copyGlyph {
1291
+ position: relative;
1292
+ width: 21px;
1293
+ height: 25px;
1294
+ display: block;
1295
+ }
1296
+ .copyGlyph::before,
1297
+ .copyGlyph::after {
1298
+ content: "";
1299
+ position: absolute;
1300
+ width: 15px;
1301
+ height: 18px;
1302
+ border: 2px solid #fff;
1303
+ border-radius: 4px;
1304
+ background: var(--echo-ink-primary);
1305
+ }
1306
+ .copyGlyph::before { left: 0; top: 0; opacity: 0.72; }
1307
+ .copyGlyph::after { right: 0; bottom: 0; }
1308
+ .agentText {
1309
+ display: block;
1310
+ min-width: 0;
1311
+ }
1312
+ .agentLaunch strong {
1313
+ display: block;
1314
+ font-family: var(--echo-font-brand);
1315
+ font-size: 17px;
1316
+ font-weight: 900;
1317
+ line-height: 1.1;
1318
+ }
1319
+ .agentText small {
1320
+ display: block;
1321
+ margin-top: 2px;
1322
+ color: var(--echo-ink-mute);
1323
+ font-size: 11px;
1324
+ line-height: 1.3;
1325
+ font-weight: 600;
1326
+ }
1327
+ .launchStatus { margin: 9px 0 0; min-height: 18px; font-size: 12px; color: var(--echo-ink-mute); }
1328
+ .hudTip { margin: 14px 0 0; max-width: 700px; display: grid; grid-template-columns: 52px 1fr; gap: 12px; align-items: center; font-size: 13px; line-height: 1.55; color: var(--echo-ink-mute); }
1329
+ .hudTip img { width: 52px; height: 52px; object-fit: contain; filter: drop-shadow(0 10px 16px rgba(26,58,143,0.18)); }
1330
+ .hudTip strong { color: var(--echo-ink-primary); }
1331
+ .hudTip code { font-family: var(--echo-font-mono); font-size: 12px; background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 5px; padding: 1px 5px; }
1332
+ .doneCloseNote { margin: clamp(30px, 5vh, 58px) 0 0; max-width: 700px; font-family: var(--echo-font-mono); font-size: 11px; line-height: 1.5; color: var(--echo-ink-faint); }
1333
+
1334
+ /* ===== Calm redesign: one focal point per state, progressive disclosure ===== */
1335
+ .calm { max-width: 640px; }
1336
+ /* Ready: a single compact meta strip replaces the giant ETA box + source cards. */
1337
+ .readyMeta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 20px 0 2px; font-family: var(--echo-font-mono); font-size: 12.5px; color: var(--echo-ink-mute); }
1338
+ .readyMeta .rm { display: inline-flex; align-items: center; gap: 7px; padding: 6px 12px; border: 1px solid var(--echo-line-soft); border-radius: 999px; background: var(--echo-paper-soft); white-space: nowrap; }
1339
+ .readyMeta .rm strong { color: var(--echo-ink-primary); font-weight: 800; }
1340
+ .readyMeta .rm img { width: 15px; height: 15px; border-radius: 4px; object-fit: contain; }
1341
+ .readyMeta .rm .sf { display: none; width: 15px; height: 15px; border-radius: 4px; background: var(--echo-ink-primary); color: #fff; font-size: 8px; font-weight: 800; align-items: center; justify-content: center; }
1342
+ /* Done: the number is the hero; a small ceremonial tick, one supporting line, no duplicate receipt. */
1343
+ .doneMark { display: inline-flex; align-items: center; gap: 9px; margin: 0; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; color: var(--echo-ink-accent-deep); }
1344
+ .doneMark .tick { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: var(--echo-ink-accent-deep); color: #fff; font-size: 13px; animation: donePop 0.5s cubic-bezier(0.2, 1.5, 0.4, 1) both; }
1345
+ @keyframes donePop { from { transform: scale(0); } to { transform: scale(1); } }
1346
+ .doneNum { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 14px; margin: 14px 0 0; }
1347
+ .doneNum strong { font-family: var(--echo-font-brand); font-size: clamp(52px, 9vw, 96px); line-height: 0.9; color: var(--echo-ink-primary); letter-spacing: -0.01em; }
1348
+ .doneNum span { font-size: clamp(15px, 1.6vw, 19px); font-weight: 700; color: var(--echo-ink-mute); }
1349
+ .doneSub { margin: 16px 0 0; max-width: 560px; font-size: clamp(15px, 1.4vw, 17px); line-height: 1.5; color: var(--echo-ink-text); }
1350
+ /* Collapse the long warm-up prompt behind a disclosure so the buttons lead. */
1351
+ .promptPeek { margin: 14px 0 0; }
1352
+ .promptPeek summary { cursor: pointer; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); list-style: none; }
1353
+ .promptPeek summary::-webkit-details-marker { display: none; }
1354
+ .promptPeek summary::before { content: "▸ "; color: var(--echo-ink-faint); }
1355
+ .promptPeek[open] summary::before { content: "▾ "; }
1356
+ .promptPeek pre { margin: 10px 0 0; white-space: pre-wrap; word-break: break-word; font-family: var(--echo-font-mono); font-size: 12px; line-height: 1.6; color: var(--echo-ink-mute); background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 8px; padding: 12px 14px; }
1357
+
1358
+ @media (max-width: 820px) {
1359
+ .exHero, .resultTop, .finishStats, .agentLaunch, .sourceGrid, .readySteps, .readyRun, .readyAction { grid-template-columns: 1fr; }
1360
+ .readyHero { grid-template-columns: 1fr; }
1361
+ .readyMascot { width: 76px; height: 76px; }
1362
+ .readyDecision .actions { justify-content: stretch; }
1363
+ .readyDecision .actions .primary { width: 100%; }
1364
+ .resultNumber span { text-align: left; }
1365
+ }
707
1366
 
708
1367
  /* ===== workspace readout dashboard (Part 2 — replaces explainPage) ===== */
709
1368
  @keyframes rdoRise { from { opacity: 0; translate: 0 14px; } to { opacity: 1; translate: 0 0; } }
@@ -856,9 +1515,13 @@ export function renderSetupPage() {
856
1515
  var decisionMade = false;
857
1516
  var connected = false;
858
1517
  var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
1518
+ var exStartAt = 0; // extraction start (ms) — drives the countdown
1519
+ var exDeadline = 0; // first honest ETA deadline; passing it while running → "taking longer" banner
1520
+ var extractionEnded = false; // user chose to leave the extraction flow before natural completion
859
1521
  var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
860
1522
  var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
861
1523
  var authUrl = "";
1524
+ var workspacePath = "";
862
1525
  var authWindow = null;
863
1526
  var connectionPollStarted = false;
864
1527
  var statsPollStarted = false;
@@ -876,6 +1539,15 @@ export function renderSetupPage() {
876
1539
  }
877
1540
  function setCityMode(enabled) {
878
1541
  document.body.classList.toggle("cityMode", !!enabled);
1542
+ if (enabled) document.body.classList.remove("extractMode", "readyMode");
1543
+ }
1544
+ function setExtractMode(enabled) {
1545
+ document.body.classList.toggle("extractMode", !!enabled);
1546
+ if (enabled) document.body.classList.remove("readyMode");
1547
+ }
1548
+ function setReadyMode(enabled) {
1549
+ document.body.classList.toggle("readyMode", !!enabled);
1550
+ if (enabled) document.body.classList.remove("cityMode", "extractMode");
879
1551
  }
880
1552
  function number(value) {
881
1553
  return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
@@ -926,8 +1598,10 @@ export function renderSetupPage() {
926
1598
  body: JSON.stringify(Object.assign({ nonce: nonce }, body || {}))
927
1599
  };
928
1600
  var res = timeoutMs ? await fetchWithTimeout(path, options, timeoutMs) : await fetch(path, options);
929
- var data = await res.json().catch(function () { return {}; });
930
- if (!res.ok) throw new Error(data.message || data.error || ("HTTP " + res.status));
1601
+ var raw = await res.text();
1602
+ var data = {};
1603
+ try { data = raw ? JSON.parse(raw) : {}; } catch (_) {}
1604
+ if (!res.ok) throw new Error(data.message || data.error || raw || ("HTTP " + res.status));
931
1605
  return data;
932
1606
  }
933
1607
  function metric(label, value, detail) {
@@ -1331,10 +2005,9 @@ export function renderSetupPage() {
1331
2005
  async function waitForStats() {
1332
2006
  if (statsPollStarted) return;
1333
2007
  statsPollStarted = true;
1334
- setCityMode(false);
1335
- setHead("Turn history into memory", "Working");
1336
- app.className = "";
1337
- app.innerHTML = '<div class="miniLoad"><h2>Preparing your conversations</h2><p>Sizing what is new since your last run.</p><div class="miniBar"><i></i></div></div>';
2008
+ // Use the same one-reading-line stage as the partial scan state. This avoids a jarring
2009
+ // old setup shell between device approval and the ready decision.
2010
+ renderDashboard();
1338
2011
  try {
1339
2012
  var started = Date.now();
1340
2013
  var lastError = "";
@@ -1369,6 +2042,8 @@ export function renderSetupPage() {
1369
2042
  }
1370
2043
  function renderDashboard(error) {
1371
2044
  setCityMode(false);
2045
+ setExtractMode(false);
2046
+ setReadyMode(true);
1372
2047
  var migratable = stats && stats.migratable ? stats.migratable : {};
1373
2048
  var pending = stats && stats.migratable ? stats.migratable.pending : null;
1374
2049
  var hasPendingCount = typeof pending === "number";
@@ -1386,18 +2061,19 @@ export function renderSetupPage() {
1386
2061
  var knownDone = pendingTrusted && pending === 0;
1387
2062
  var canExtract = pendingTrusted && pending > 0;
1388
2063
  var pendN = hasPendingCount ? pending : 0;
1389
- var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
1390
2064
  var etaLabel = eta.estimatedLabel || migratable.estimatedLabel || "a couple of minutes";
1391
- setHead("Turn history into memory", localScanReady ? "Ready" : "Scanning");
2065
+ var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
2066
+ var pendingClaudeCode = typeof migratable.pendingClaudeCode === "number" ? migratable.pendingClaudeCode : null;
2067
+ setHead("Turn coding history into memory", pendingTrusted ? (knownDone ? "Done" : "Ready") : (localScanReady ? "Ready" : "Scanning"));
1392
2068
  var headline = !pendingTrusted
1393
2069
  ? "Counting your new conversations…"
1394
- : (knownDone ? "You are all caught up." : (pendN === 1 ? "1 new conversation to turn into memory." : number(pendN) + " new conversations to turn into memory."));
2070
+ : (knownDone ? "You are all caught up." : "Echo found " + number(pendN) + " new coding session" + (pendN === 1 ? "" : "s") + ".");
1395
2071
  var sub = !pendingTrusted
1396
2072
  ? "Echo is matching your local history against what is already in memory."
1397
2073
  : (knownDone
1398
- ? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
1399
- : "Already-processed conversations are skipped, so re-running stays incremental. About " + etaLabel + ".");
1400
- var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? ("Extract " + number(pendN) + " conversation" + (pendN === 1 ? "" : "s")) : "All caught up");
2074
+ ? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing, so Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
2075
+ : "Turn them into memory your agents can recall: decisions, fixes, and where you left off. No more re-explaining your project every session.");
2076
+ var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? "Start extraction" : "All caught up");
1401
2077
  // Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
1402
2078
  // (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
1403
2079
  // Later polls only patch the text/counts in place; the iframe stays mounted.
@@ -1405,49 +2081,89 @@ export function renderSetupPage() {
1405
2081
  if (!live) {
1406
2082
  extractMounted = false;
1407
2083
  app.className = "";
2084
+ // One reading line, top to bottom: the find (headline) → the value (sub) → quiet proof
2085
+ // (source strip) → the single action with its reassurances attached → optional detail.
2086
+ // The 19/9 split is EVIDENCE ("Echo scanned MY machine"), not data to study — so it is a
2087
+ // one-line strip, not stat cards; the ETA is a commitment-moment note under the button,
2088
+ // not a display-size headline.
1408
2089
  app.innerHTML =
1409
2090
  '<div id="exBanner"></div>' +
1410
- '<section class="exHero">' +
1411
- '<div class="exCopy">' +
1412
- '<p class="kicker-mono" id="exKicker"></p>' +
1413
- '<h2 id="exHeadline"></h2>' +
1414
- '<p class="exSub" id="exSub"></p>' +
1415
- '<div class="exLegend" id="exLegend"></div>' +
2091
+ '<section class="readyDecision">' +
2092
+ '<div class="readyHero">' +
2093
+ '<img class="readyMascot" src="/hud-assets/echo-face-cutout.png" alt="" />' +
2094
+ '<div class="exCopy">' +
2095
+ '<h2 id="exHeadline"></h2>' +
2096
+ '<p class="exSub" id="exSub"></p>' +
2097
+ '</div>' +
2098
+ '</div>' +
2099
+ '<div class="proofStrip" id="exSources"></div>' +
2100
+ '<div class="readyGo">' +
1416
2101
  '<div class="actions">' +
1417
2102
  '<button id="migrate" class="primary"></button>' +
1418
- '<button id="skip" class="secondary"></button>' +
1419
2103
  '</div>' +
1420
- '<p class="exHint">No transcripts leave this machine until you start extraction.</p>' +
2104
+ '<p class="ctaMeta" id="exEta"></p>' +
2105
+ '</div>' +
2106
+ '<div class="readyFoot">' +
2107
+ '<details class="readyExplain" id="readyExplain"></details>' +
1421
2108
  '</div>' +
1422
- plateBlock() +
1423
2109
  '</section>';
1424
2110
  document.getElementById("migrate").onclick = startMigrate;
1425
- document.getElementById("skip").onclick = skip;
1426
2111
  dashMounted = true;
1427
2112
  }
1428
2113
  document.getElementById("exBanner").innerHTML =
1429
2114
  (statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask the bridge to extract anything unprocessed.</div>' : '') +
1430
2115
  (error ? '<div class="error">' + esc(error) + '</div>' : '');
1431
- document.getElementById("exKicker").textContent = "Local scan · " + number(sessions.total) + " sessions found";
1432
2116
  document.getElementById("exHeadline").textContent = headline;
1433
2117
  document.getElementById("exSub").textContent = sub;
1434
- document.getElementById("exLegend").innerHTML = pendingTrusted
1435
- ? '<span class="lgDot lgNew"></span>new (' + esc(number(pendN)) + ')<span class="lgDot lgDim"></span>already in memory (' + esc(number(alreadyN)) + ')'
1436
- : '<span class="lgDot lgDim"></span>matching your history against saved memory…';
1437
- // While still counting, hide the primary button entirely (a disabled "Counting…" button looked
1438
- // half-baked) the headline + legend already say it is working; only "Not now" stays available.
2118
+ var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
2119
+ var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
2120
+ var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
2121
+ // Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
2122
+ var srcIcon = function (id, fallback) {
2123
+ return '<span class="pfIcon"><img src="/agent-icons/' + id + '.png" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="pfFallback">' + fallback + '</span></span>';
2124
+ };
2125
+ document.getElementById("exSources").innerHTML = !pendingTrusted
2126
+ ? '<span class="pfNote">Scanning local history&hellip;</span>'
2127
+ : (canExtract
2128
+ ? '<span class="pf">' + srcIcon("codex", "CX") + '<strong>' + esc(number(codexN)) + '</strong> Codex</span>' +
2129
+ '<span class="pfDot">&middot;</span>' +
2130
+ '<span class="pf">' + srcIcon("claude-desktop", "CL") + '<strong>' + esc(number(claudeN)) + '</strong> Claude Code</span>' +
2131
+ '<span class="pfNote">found on this Mac</span>'
2132
+ : "");
2133
+ // Reassurances live at the moment of commitment — right under the button.
2134
+ document.getElementById("exEta").innerHTML = pendingTrusted
2135
+ ? (canExtract
2136
+ ? '<span class="cm">&asymp; ' + esc(etaLabel) + '</span><span class="cmDot">&middot;</span><span class="cm">runs locally</span>' + (alreadyN ? '<span class="cmDot">&middot;</span><span class="cm">' + esc(number(alreadyN)) + ' already-saved skipped</span>' : '')
2137
+ : 'Nothing new to extract right now.')
2138
+ : 'Echo is checking local history against saved memory&hellip;';
2139
+ document.getElementById("readyExplain").innerHTML =
2140
+ '<summary>What exactly happens?</summary>' +
2141
+ '<p>A coding session is a local Codex or Claude Code transcript from this Mac. Echo extracts compact memories from it, not a full chat replay.</p>' +
2142
+ '<div class="readySteps">' +
2143
+ '<div class="readyStep"><strong>Read locally</strong><span>Echo scans session logs on this device.</span></div>' +
2144
+ '<div class="readyStep"><strong>Skip repeats</strong><span>Anything already in memory is ignored.</span></div>' +
2145
+ '<div class="readyStep"><strong>Save memory</strong><span>Decisions, fixes, context, and next steps become searchable.</span></div>' +
2146
+ '</div>';
2147
+ // While still counting, hide the primary button entirely; the headline + source split
2148
+ // already say it is working, and the ready state should present one clear action.
1439
2149
  var migrateBtn = document.getElementById("migrate");
1440
- if (pendingTrusted) { migrateBtn.style.display = ""; migrateBtn.textContent = extractLabel; migrateBtn.disabled = !canExtract; }
1441
- else { migrateBtn.style.display = "none"; }
1442
- document.getElementById("skip").textContent = knownDone ? "Finish setup" : "Not now";
2150
+ if (pendingTrusted) {
2151
+ migrateBtn.style.display = "";
2152
+ migrateBtn.disabled = false;
2153
+ migrateBtn.textContent = canExtract ? extractLabel : "Finish setup";
2154
+ migrateBtn.onclick = canExtract ? startMigrate : skip;
2155
+ } else {
2156
+ migrateBtn.style.display = "none";
2157
+ }
1443
2158
  }
1444
2159
  async function startMigrate() {
1445
2160
  if (decisionMade) return;
1446
2161
  decisionMade = true;
2162
+ extractionEnded = false;
1447
2163
  setCityMode(false);
1448
2164
  setHead("Extracting", "Starting");
1449
2165
  app.className = "";
1450
- app.innerHTML = '<div class="miniLoad"><h2>Starting extraction</h2><p>Echo is sizing the remaining jobs, then imports automatically keep this terminal open.</p><div class="miniBar"><i></i></div></div>';
2166
+ app.innerHTML = '<div class="miniLoad"><h2>Starting extraction</h2><p>Echo is sizing the remaining jobs, then imports automatically. Finished work is saved as it goes.</p><div class="miniBar"><i></i></div></div>';
1451
2167
  try {
1452
2168
  var started = await postJson("/migrate", {});
1453
2169
  extractMounted = false;
@@ -1460,14 +2176,18 @@ export function renderSetupPage() {
1460
2176
  }
1461
2177
  async function pollProgress() {
1462
2178
  for (;;) {
2179
+ if (extractionEnded) return;
1463
2180
  var progress = await getJson("/progress");
2181
+ if (extractionEnded) return;
1464
2182
  renderProgress(progress);
1465
2183
  if (progress.status === "completed" || progress.status === "failed") return;
1466
2184
  await sleep(1200);
1467
2185
  }
1468
2186
  }
1469
2187
  function renderProgress(progress) {
2188
+ if (extractionEnded) return;
1470
2189
  setCityMode(false);
2190
+ setExtractMode(true);
1471
2191
  var status = progress.status;
1472
2192
  var done = status === "completed";
1473
2193
  var stopped = status === "failed";
@@ -1487,39 +2207,199 @@ export function renderSetupPage() {
1487
2207
  '</section>';
1488
2208
  extractMounted = true;
1489
2209
  }
2210
+ var shell = app.querySelector(".exHero");
2211
+ if (shell) {
2212
+ shell.classList.toggle("exHeroResult", done || stopped);
2213
+ shell.classList.toggle("exHeroActive", !done && !stopped);
2214
+ }
1490
2215
  var t = document.getElementById("exText");
1491
2216
  if (!t) return;
1492
2217
  if (done || stopped) {
1493
2218
  setHead(done ? "Memory captured" : "Extraction stopped", done ? "Done" : "Needs attention");
2219
+ var memoryNoun = extracted === 1 ? "new memory" : "new memories";
2220
+ var conversationNoun = completed === 1 ? "conversation" : "conversations";
2221
+ var stopDetail = String(progress.error) === "key-expired"
2222
+ ? "Your encryption key was not accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
2223
+ : esc(progress.error || "Re-run extraction to finish the rest. Already-done conversations are skipped.");
1494
2224
  t.innerHTML =
1495
- '<p class="kicker-mono">' + (done ? "Extraction complete" : "Extraction stopped") + '</p>' +
1496
- '<h2>' + (done ? esc(number(extracted)) + ' new ' + (extracted === 1 ? "memory" : "memories") + '.' : "Some conversations did not finish.") + '</h2>' +
1497
- '<p class="exSub">' + (done
1498
- ? "From " + esc(number(completed)) + " conversation" + (completed === 1 ? "" : "s") + ". Your next coding session recalls instead of re-reading — and it keeps compounding."
1499
- : (String(progress.error) === "key-expired"
1500
- ? "Your encryption key was not accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
1501
- : esc(progress.error || "Re-run extraction to finish the rest — already-done conversations are skipped."))) + '</p>' +
1502
- '<div class="recon">&#10003; ' + esc(number(completed)) + ' extracted · ' + esc(number(extracted)) + ' memories · ' + esc(number(failed)) + ' error' + (failed === 1 ? "" : "s") + '</div>';
2225
+ '<div class="resultStage ' + (done ? "ok" : "warn") + '">' +
2226
+ (done ? "" : '<p class="kicker-mono">Extraction stopped</p>') +
2227
+ '<div class="resultTop">' +
2228
+ '<div>' +
2229
+ '<h2>' + (done ? "Memory is live." : "Some conversations need another pass.") + '</h2>' +
2230
+ (done ? "" : '<p class="exSub">' + stopDetail + '</p>') +
2231
+ '<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>' +
2232
+ '</div>' +
2233
+ '</div>' +
2234
+ (done ? tryItCard() : "") +
2235
+ '<div class="memoryReceipt">' +
2236
+ '<p class="receiptK">What just happened</p>' +
2237
+ '<div class="finishStats">' +
2238
+ '<div class="finishStat"><strong>' + esc(number(completed)) + '</strong><span>' + esc(conversationNoun) + ' extracted</span></div>' +
2239
+ '<div class="finishStat"><strong>' + esc(number(extracted)) + '</strong><span>memories ready</span></div>' +
2240
+ '</div>' +
2241
+ '</div>' +
2242
+ '</div>' +
2243
+ (done ? hudTip() + doneCloseNote() : "");
2244
+ var copyBtn = document.getElementById("tryCopy");
2245
+ if (copyBtn) copyBtn.addEventListener("click", copyPromptOnly);
2246
+ var codexBtn = document.getElementById("tryCodex");
2247
+ if (codexBtn) codexBtn.addEventListener("click", tryInCodex);
2248
+ var claudeBtn = document.getElementById("tryClaudeDesktop");
2249
+ if (claudeBtn) claudeBtn.addEventListener("click", openClaudeDesktop);
1503
2250
  return;
1504
2251
  }
1505
2252
  setHead(preparing ? "Preparing" : "Extracting", preparing ? "Preparing" : "Extracting");
1506
- var statusLine = progress.latest ? ("Now: " + progress.latest) : (preparing ? "Getting your sessions ready locally…" : "");
2253
+ var checkingEdgeCases = failed > 0 && !preparing;
2254
+ var statusLine = checkingEdgeCases
2255
+ ? "Finishing a few checks without stopping the import."
2256
+ : (progress.latest ? (progress.latest + (progress.latestRepo ? " · " + progress.latestRepo : "")) : (preparing ? "Getting your sessions ready locally…" : "Importing the next batch…"));
2257
+ // Honest countdown: first ETA from batch size (throughput is rate-capped ≈ 26/min), then
2258
+ // refined from the observed rate. Blowing past the FIRST estimate flips on the reassurance banner.
2259
+ var countdownHtml = "";
2260
+ var overdueHtml = "";
2261
+ if (!preparing && total > 0) {
2262
+ var now = Date.now();
2263
+ if (!exStartAt) { exStartAt = now; exDeadline = now + Math.max(60, total * 2.6) * 1000; }
2264
+ var elapsed = (now - exStartAt) / 1000;
2265
+ var remainSec;
2266
+ if (completed >= 5 && elapsed > 20) remainSec = (total - completed) / (completed / elapsed);
2267
+ else remainSec = Math.max(0, (exDeadline - now) / 1000);
2268
+ if (now > exDeadline) {
2269
+ overdueHtml = '<div class="exOverdue"><strong>Taking longer than expected. Still working.</strong>' +
2270
+ 'Everything already processed is saved. Ending now is safe; the next run resumes from the remaining conversations.</div>';
2271
+ } else {
2272
+ countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes · safe to end and resume</p>';
2273
+ }
2274
+ }
1507
2275
  t.innerHTML =
1508
- '<p class="kicker-mono">' + (preparing ? "Preparing" : "Extraction progress") + '</p>' +
1509
- '<h2>' + (preparing ? "Sizing your sessions…" : (total ? esc(number(completed)) + ' of ' + esc(number(total)) : "Starting…")) + '</h2>' +
1510
- '<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
1511
- '<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") + ' captured' + (failed ? ' · ' + esc(number(failed)) + ' failed' : "") + '.' + (statusLine ? ' ' + esc(statusLine) : "") + '</p>' +
1512
- (progress.capped ? '<p class="exHint">Importing the newest ' + esc(number(progress.capped)) + ' re-run later for older sessions.</p>' : '<p class="exHint">Keep this terminal open nothing leaves your machine beyond what you extract. You can use EchoMem in your editor now.</p>');
2276
+ '<div class="workbench">' +
2277
+ '<h2>' + (preparing ? "Setting up the import." : "Building your memory.") + '</h2>' +
2278
+ '<div class="exMeter"><strong>' + (preparing ? "Local" : esc(number(pct)) + "%") + '</strong><span>' + (preparing ? "sizing sessions" : (total ? esc(number(completed)) + " of " + esc(number(total)) + " conversations" : "starting")) + '</span></div>' +
2279
+ '<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
2280
+ '<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") + ' captured' + (checkingEdgeCases ? ' · finishing checks' : "") + '.</p>' +
2281
+ '<div class="exCurrent"><i></i><div><span>Now handling</span><strong>' + esc(statusLine) + '</strong></div></div>' +
2282
+ countdownHtml + overdueHtml +
2283
+ '<div class="extractEndRow"><button type="button" class="secondary" id="endExtraction">End for now</button><span>Ends cleanly; next time continues from saved work.</span></div>' +
2284
+ '<p class="exHint">You can step away from this page. Echo saves each finished conversation as it goes.</p>' +
2285
+ '</div>';
2286
+ var endBtn = document.getElementById("endExtraction");
2287
+ if (endBtn) endBtn.onclick = endExtraction;
2288
+ }
2289
+ function fmtCountdown(sec) {
2290
+ sec = Math.max(0, Math.round(sec));
2291
+ var m = Math.floor(sec / 60), s = sec % 60;
2292
+ if (m >= 60) { var h = Math.floor(m / 60); return h + "h " + (m % 60) + "m"; }
2293
+ return m > 0 ? m + "m " + s + "s" : s + "s";
2294
+ }
2295
+ function recallPrompt() {
2296
+ return "Warm up from EchoMem: call search_memories for what I have been working on recently, then give me a 5-line recap of my current projects and where each one stands, so we can pick up right where I left off. If this is Codex or Claude Code, open the EchoMem context HUD by running echomem-hud app. Use the HUD's context-health score to tell me when to renew into a fresh session and carry a checkpoint forward. As we work, when we make a decision, finish a fix, or hit a milestone, save it with save_conversation. Use this prompt in Codex, Claude Code, or Claude Desktop; if you are in another chat surface, tell me EchoMem tools may not be available here.";
2297
+ }
2298
+ function setLaunchStatus(message) {
2299
+ var node = document.getElementById("launchStatus");
2300
+ if (node) node.textContent = message || "";
2301
+ }
2302
+ async function copyRecallPrompt() {
2303
+ var prompt = recallPrompt();
2304
+ try {
2305
+ await navigator.clipboard.writeText(prompt);
2306
+ return true;
2307
+ } catch (_) {
2308
+ return false;
2309
+ }
2310
+ }
2311
+ async function copyPromptOnly() {
2312
+ var copied = await copyRecallPrompt();
2313
+ setLaunchStatus(copied ? "Prompt copied." : "Could not copy automatically. Select the prompt and copy it.");
2314
+ }
2315
+ function codexDeepLink(prompt) {
2316
+ var q = "prompt=" + encodeURIComponent(prompt);
2317
+ if (workspacePath) q += "&path=" + encodeURIComponent(workspacePath);
2318
+ return "codex://new?" + q;
2319
+ }
2320
+ async function tryInCodex() {
2321
+ var prompt = recallPrompt();
2322
+ var copied = await copyRecallPrompt();
2323
+ setLaunchStatus(copied ? "Copied prompt. Opening Codex..." : "Opening Codex. If the prompt is empty, copy it manually.");
2324
+ window.location.href = codexDeepLink(prompt);
2325
+ }
2326
+ async function openClaudeDesktop() {
2327
+ var copied = await copyRecallPrompt();
2328
+ setLaunchStatus(copied ? "Copied prompt. Opening Claude Desktop..." : "Opening Claude Desktop. If the prompt is empty, copy it manually.");
2329
+ try {
2330
+ var res = await postJson("/launch-agent", { agent: "claude-desktop" }, 8000);
2331
+ setLaunchStatus(res && res.message ? res.message : "Prompt copied. Paste it into Claude Desktop.");
2332
+ } catch (error) {
2333
+ var msg = error && error.message ? error.message : "Could not auto-open Claude.";
2334
+ if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The prompt is copied - open Claude Desktop and paste it.";
2335
+ setLaunchStatus(msg);
2336
+ }
2337
+ }
2338
+ function tryItCard() {
2339
+ var prompt = recallPrompt();
2340
+ return '<div class="tryCard">' +
2341
+ '<p class="tryK">Try it now</p>' +
2342
+ '<h3 class="tryTitle">Ask your agent to pick up where you left off.</h3>' +
2343
+ '<pre id="tryPrompt">' + esc(prompt) + '</pre>' +
2344
+ '<div class="agentLaunch" aria-label="Open in a coding agent">' +
2345
+ '<button type="button" id="tryCodex"><span class="agentIcon"><img src="/agent-icons/codex.png" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="agentFallback">CX</span></span><span class="agentText"><strong>Try in Codex</strong><small>copy prompt + open session</small></span></button>' +
2346
+ '<button type="button" id="tryClaudeDesktop"><span class="agentIcon"><img src="/agent-icons/claude-desktop.png" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="agentFallback">CL</span></span><span class="agentText"><strong>Open Claude Desktop</strong><small>copy prompt + open app</small></span></button>' +
2347
+ '<button type="button" id="tryCopy"><span class="agentIcon"><span class="copyGlyph" aria-hidden="true"></span></span><span class="agentText"><strong>Copy prompt</strong><small>paste anywhere</small></span></button>' +
2348
+ '</div>' +
2349
+ '<p class="launchStatus" id="launchStatus">Paste into Codex, Claude Code, or Claude Desktop. EchoMem tools need to be available there.</p>' +
2350
+ '</div>';
2351
+ }
2352
+ function hudTip() {
2353
+ return '<p class="hudTip"><img src="/hud-assets/echo-face-cutout.png" alt="" /><span><strong>Live HUD:</strong> Echo watches the context window while you work. When the score drifts low, start a fresh session and carry the checkpoint forward. In Codex or Claude Code, ask &ldquo;<strong>open the EchoMem HUD</strong>&rdquo; or run <code>echomem-hud app</code>.</span></p>';
2354
+ }
2355
+ function doneCloseNote() {
2356
+ return '<p class="doneCloseNote">You can safely close this page. EchoMem is set up, and your memories are saved.</p>';
2357
+ }
2358
+ function renderEnding() {
2359
+ setCityMode(false);
2360
+ setExtractMode(true);
2361
+ setHead("Ending extraction", "Ending");
2362
+ app.className = "";
2363
+ app.innerHTML =
2364
+ '<div class="miniLoad" style="margin:0 auto;text-align:center;place-items:center">' +
2365
+ '<h2>Ending cleanly</h2>' +
2366
+ '<p>Give Echo a moment to mark this setup as finished for now. Everything already captured stays saved.</p>' +
2367
+ '<div class="miniBar"><i></i></div>' +
2368
+ '</div>';
2369
+ }
2370
+ function renderSkipped() {
2371
+ setCityMode(false);
2372
+ setExtractMode(true);
2373
+ setHead("Setup paused", "Skipped");
2374
+ app.className = "";
2375
+ app.innerHTML =
2376
+ '<div class="skipStage">' +
2377
+ '<span class="seal">Skipped for now</span>' +
2378
+ '<h2>You can come back anytime.</h2>' +
2379
+ '<p>Echo saved what already finished. When you are ready, run <code>echomem-mcp migrate</code> and it will continue from the remaining conversations.</p>' +
2380
+ hudTip() +
2381
+ '</div>';
2382
+ }
2383
+ async function endExtraction() {
2384
+ if (extractionEnded) return;
2385
+ var ok = window.confirm("End extraction for now? You can come back anytime. Everything already captured stays saved, and the next run continues from what remains.");
2386
+ if (!ok) return;
2387
+ extractionEnded = true;
2388
+ renderEnding();
2389
+ try {
2390
+ await postJson("/skip", {});
2391
+ } catch (_) {
2392
+ /* The bridge may close while ending; the local page can still show the paused state. */
2393
+ }
2394
+ await sleep(650);
2395
+ renderSkipped();
1513
2396
  }
1514
2397
  async function skip() {
1515
2398
  if (decisionMade) return;
1516
2399
  decisionMade = true;
1517
2400
  try {
1518
2401
  await postJson("/skip", {});
1519
- setCityMode(false);
1520
- setHead("Setup complete", "Done");
1521
- app.className = "notice";
1522
- app.innerHTML = '<h2 style="color:#203820">All set</h2><p style="margin-top:10px">You can extract unprocessed conversations later with echomem-mcp migrate.</p>';
2402
+ renderSkipped();
1523
2403
  } catch (e) {
1524
2404
  decisionMade = false;
1525
2405
  renderDashboard(e && e.message);
@@ -1605,15 +2485,53 @@ export function renderSetupPage() {
1605
2485
  getJson("/config").then(function (cfg) {
1606
2486
  authUrl = cfg.authUrl || authUrl;
1607
2487
  connected = !!cfg.connected;
2488
+ workspacePath = cfg.workspacePath || workspacePath;
1608
2489
  }).catch(function () {});
1609
2490
  // Scan-first: show the local forensic report before asking the user to connect.
1610
2491
  await showReport();
1611
2492
  }
1612
- init().catch(function (e) {
1613
- setHead("Connection issue", "Needs attention");
1614
- app.className = "error";
1615
- app.textContent = e && e.message ? e.message : String(e);
1616
- });
2493
+ // Dev/design preview: ?preview=extract-counting|extract-ready|extract-run|extract-overdue|extract-done|extract-ending|extract-skipped renders that extraction
2494
+ // state directly with sample data (no server, no nonce). Lets us eyeball every state of the
2495
+ // first-impression flow without running a real 3000-session import.
2496
+ var previewState = params.get("preview");
2497
+ if (previewState === "extract-counting") {
2498
+ stats = null;
2499
+ renderDashboard();
2500
+ } else if (previewState === "extract-ready") {
2501
+ stats = {
2502
+ sessions: { total: 136, codex: 82, claudeCode: 54 },
2503
+ migratable: {
2504
+ pending: 28,
2505
+ pendingTotal: 28,
2506
+ pendingCodex: 19,
2507
+ pendingClaudeCode: 9,
2508
+ alreadyMigrated: 108,
2509
+ skippedActive: 0,
2510
+ eta: { estimatedLabel: "under 2 minutes" },
2511
+ estimatedLabel: "under 2 minutes",
2512
+ },
2513
+ discovery: { phase: "account", exact: false },
2514
+ };
2515
+ renderDashboard();
2516
+ } else if (previewState === "extract-run") {
2517
+ renderProgress({ status: "running", total: 28, completed: 4, extracted: 19, failed: 1, latest: "codex 2026-07-01", latestRepo: "EchoMem-Chrome" });
2518
+ } else if (previewState === "extract-overdue") {
2519
+ exStartAt = Date.now() - 200000;
2520
+ exDeadline = Date.now() - 1000;
2521
+ renderProgress({ status: "running", total: 28, completed: 21, extracted: 96, failed: 2, latest: "claude-code 2026-06-28", latestRepo: "DG-Web" });
2522
+ } else if (previewState === "extract-done") {
2523
+ renderProgress({ status: "completed", total: 28, completed: 28, extracted: 128, failed: 0 });
2524
+ } else if (previewState === "extract-ending") {
2525
+ renderEnding();
2526
+ } else if (previewState === "extract-skipped") {
2527
+ renderSkipped();
2528
+ } else {
2529
+ init().catch(function (e) {
2530
+ setHead("Connection issue", "Needs attention");
2531
+ app.className = "error";
2532
+ app.textContent = e && e.message ? e.message : String(e);
2533
+ });
2534
+ }
1617
2535
  })();
1618
2536
  </script>
1619
2537
  </body>